巧妙地穿梭双端:掌握C++ STL中deque容器的强大功能
- 引言
- 一、deque容器概述
- 二、deque容器实现原理
- 三、deque容器常用API
- 3.1、deque的构造函数
- 3.2、deque的赋值操作
- 3.3、deque的大小操作
- 3.4、deque的双端插入和删除操作
- 3.5、deque的数据存取
- 3.6、deque的插入操作
- 3.7、deque的删除操作
- 四、deque容器在竞技类中的应用案例
- 总结
引言
💡 作者简介:一个热爱分享高性能服务器后台开发知识的博主,目标是通过理论与代码实践的结合,让世界上看似难以掌握的技术变得易于理解与掌握。技能涵盖了多个领域,包括C/C++、Linux、Nginx、MySQL、Redis、fastdfs、kafka、Docker、TCP/IP、协程、DPDK等。
👉
🎖️ CSDN实力新星、CSDN博客专家
👉
🔔 专栏介绍:从零到c++精通的学习之路。内容包括C++基础编程、中级编程、高级编程;掌握各个知识点。
👉
🔔 专栏地址:C++从零开始到精通
👉
🔔 博客主页:https://blog.csdn.net/Long_xu
🔔 上一篇:【039】掌握Vector容器:C++中最强大的动态数组
一、deque容器概述
deque(双端队列)是C++ STL(Standard Template Library)中的一个容器,它提供了高效的插入和删除操作,并且支持在两端进行快速访问的能力。
与vector相比,deque具有更好的插入和删除性能,尤其是在容器的前端。这是因为deque在内部实现上采用了一块连续的存储区,并结合了多个缓冲区,使得插入和删除操作能够在任意位置进行,而不会导致整个容器的元素移动。
Vector容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector容器也可以在头尾两端插入元素,但是在其头部操作效率奇差,无法被接受。
deque还具备以下特点:
- 双向访问:deque允许高效地在队首和队尾执行插入、删除和访问操作,无论容器的大小如何。
- 动态扩展:deque可以根据需要自动调整内部的缓冲区大小,因此可以灵活地适应不同的数据规模。
- 随机访问:类似于数组和vector,deque支持通过索引快速访问特定位置的元素。
- 迭代器失效:插入和删除操作可能会导致迭代器失效,因此在使用迭代器时需要格外小心。
使用deque时,可以利用其快速插入和删除的特性处理大量数据或者需要频繁在两端操作的场景。相比其他容器,deque提供了更加灵活和高效的元素管理,并且可以根据需要动态调整内部的存储空间。
要使用deque容器,需要包含头文件<deque>
,并使用std命名空间或者使用using namespace std;
进行简化操作。
Deque容器和vector容器最大的差异:
- 一在于deque允许使用常数项时间对头端进行元素的插入和删除操作。
- 二在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,"I旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间’这样的事情在deque身上是不会发生的。也因此,deque没有必须要提供所谓的空间保留(reserve)功能.虽然deque容器也提供了Random Access lterator,但是它的迭代器并不是普通的指针,其复杂度和vector不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该尽可能的使用vector,而不是deque。
对deque进行的排序操作,为了最高效率,可将deque先完整的复制到一个vector中,对vector容器进行排序,再复制回deque。
二、deque容器实现原理
Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长却只能向尾端成长,而且其成长其实是一个假象,事实上(1)申请更大空间(⑵)原数据复制新空间(⑶释放原空间三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。
Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
Deque代码的实现远比vector或list都多得多。Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的 map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。
deque(双端队列)在C++ STL中的实现原理如下:
-
内存结构:deque使用了一块连续的存储区,其中每个元素被称为缓冲区(buffer)。每个缓冲区都是固定大小的,并且包含多个节点(node),每个节点存储一个元素。
-
头尾迭代器:deque维护了两个迭代器,即头部迭代器(begin)和尾部迭代器(end)。这两个迭代器指向不同的缓冲区,并用于访问deque的首元素和尾元素。
-
缓冲区管理:deque采用了一个索引表(map)来记录缓冲区的位置和大小。索引表中每个元素都指向一个缓冲区,并保存了该缓冲区的起始位置、容量以及节点数。
-
增长策略:当需要在deque的前端或后端插入新元素时,deque会根据以下策略进行内存增长:
- 当前端空间不足时,在头部添加一个新的缓冲区,并更新索引表。
- 当后端空间不足时,在尾部添加一个新的缓冲区,并更新索引表。
- 如果之前已经分配过一块较大的缓冲区,且其剩余空间足够容纳新元素,则会在该缓冲区中直接插入。
-
缓冲区大小:对于deque的每个缓冲区,其节点数目通常为2的幂次方。这是因为使用二进制位来表示节点索引可以通过位运算实现快速的计算。
通过上述机制,deque实现了在两端进行高效的插入和删除操作。当插入或删除操作导致需要添加或移除缓冲区时,deque会根据增长策略进行适当的内存调整,并更新索引表以保持deque的整体结构有效。
注意:由于deque允许在任意位置进行插入和删除操作,而不仅限于头部和尾部,因此迭代器的失效情况相对复杂一些,需要格外注意使用迭代器的情况。
三、deque容器常用API
3.1、deque的构造函数
在C++中,deque(双端队列)的构造函数原型如下:
-
默认构造函数:
deque();
该构造函数创建一个空的deque对象。
-
带有初始元素和默认值的构造函数:
deque(size_type count, const T& value = T());
该构造函数创建一个包含count个元素的deque对象,并将每个元素初始化为value。
-
带有初始元素范围的构造函数:
template <class InputIterator> deque(InputIterator first, InputIterator last);
该构造函数创建一个包含范围在[first, last)之间的元素的deque对象。参数first和last可以是指向序列的迭代器。
-
拷贝构造函数:
deque(const deque& other);
该构造函数创建一个与其他deque对象相同的副本。
-
移动构造函数:
deque(deque&& other) noexcept;
该构造函数创建一个新的deque对象,通过移动其他deque对象的内容。
使用示例:
-
使用默认构造函数创建空的deque:
deque<int> myDeque; // 创建一个空的deque对象
-
使用带有初始元素和默认值的构造函数创建具有特定大小和初始值的deque:
deque<int> myDeque(5, 10); // 创建一个包含5个元素,每个元素初始化为10的deque对象
-
使用带有初始元素范围的构造函数创建deque:
vector<int> vec = {1, 2, 3, 4, 5}; deque<int> myDeque(vec.begin(), vec.end()); // 根据vector的内容创建一个相应的deque对象
-
使用拷贝构造函数创建副本:
deque<int> originalDeque = {1, 2, 3}; deque<int> copiedDeque(originalDeque); // 创建一个originalDeque的副本
-
使用移动构造函数:
deque<int> tempDeque {1, 2, 3, 4, 5}; deque<int> movedDeque(std::move(tempDeque)); // 移动tempDeque创建一个新的deque
3.2、deque的赋值操作
-
assign() 函数原型:
void assign(size_type count, const T& value); template<class InputIt> void assign(InputIt first, InputIt last);
-
= (拷贝赋值运算符)函数原型:
deque& operator=(const deque& other);
-
swap() 函数原型:
void swap(deque& other) noexcept;
使用示例:
-
assign() 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque; // 使用 assign 为 deque 赋值 myDeque.assign(5, 42); // 将5个值为42的元素赋给 deque // 输出 deque 的内容:42, 42, 42, 42, 42 for (const auto& element : myDeque) { std::cout << element << " "; } std::cout << std::endl; return 0; }
-
=(拷贝赋值运算符)示例:
#include <iostream> #include <deque> int main() { std::deque<int> deque1 {1, 2, 3}; std::deque<int> deque2 {4, 5, 6}; deque2 = deque1; // 将deque1拷贝给deque2 // 输出 deque2 的内容:1, 2, 3 for (const auto& element : deque2) { std::cout << element << " "; } std::cout << std::endl; return 0; }
-
swap() 示例:
#include <iostream> #include <deque> int main() { std::deque<int> deque1 {1, 2, 3}; std::deque<int> deque2 {4, 5, 6}; deque1.swap(deque2); // 交换 deque1 和 deque2 的内容 // 输出 deque1 的内容:4, 5, 6 for (const auto& element : deque1) { std::cout << element << " "; } std::cout << std::endl; return 0;
3.3、deque的大小操作
C++ deque(双端队列)提供了一些函数来获取和修改队列的大小。以下是deque大小操作的函数原型和使用示例:
-
size()
函数:- 函数原型:
size_type size() const noexcept;
- 功能:返回deque中元素的数量。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; size_t size = myDeque.size(); // 输出deque的大小:5 std::cout << "Size of deque: " << size << std::endl; return 0; }
- 函数原型:
-
empty()
函数:- 函数原型:
bool empty() const noexcept;
- 功能:检查deque是否为空。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque; if (myDeque.empty()) { std::cout << "Deque is empty." << std::endl; } else { std::cout << "Deque is not empty." << std::endl; } return 0; }
- 函数原型:
-
resize()
函数:- 函数原型:
void resize(size_type count);
void resize(size_type count, const value_type& value);
- 功能:调整deque的大小为指定的数量。如果新的大小比当前大小大,则在末尾插入默认的或指定的值;如果新的大小比当前大小小,则删除末尾的元素。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; myDeque.resize(8); // 将deque的大小调整为8,新增的元素使用默认值0填充 // 输出调整后的deque:1, 2, 3, 4, 5, 0, 0, 0 for (const auto& element : myDeque) { std::cout << element << " "; } std::cout << std::endl; return 0; }
- 函数原型:
3.4、deque的双端插入和删除操作
C++ deque(双端队列)提供了一些函数来进行双端插入和删除操作。
-
push_front()
函数:- 函数原型:
void push_front(const T& value);
- 功能:在deque的前端插入一个元素。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {2, 3, 4}; myDeque.push_front(1); // 在deque的前端插入元素1 // 输出调整后的deque:1, 2, 3, 4 for (const auto& element : myDeque) { std::cout << element << " "; } std::cout << std::endl; return 0; }
- 函数原型:
-
pop_front()
函数:- 函数原型:
void pop_front();
- 功能:从deque的前端删除一个元素。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; myDeque.pop_front(); // 删除deque的第一个元素 // 输出调整后的deque:2, 3, 4, 5 for (const auto& element : myDeque) { std::cout << element << " "; } std::cout << std::endl; return 0; }
- 函数原型:
-
push_back()
函数:- 函数原型:
void push_back(const T& value);
- 功能:在deque的末尾插入一个元素。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3}; myDeque.push_back(4); // 在deque的末尾插入元素4 // 输出调整后的deque:1, 2, 3, 4 for (const auto& element : myDeque) { std::cout << element << " "; } std::cout << std::endl; return 0; }
- 函数原型:
-
pop_back()
函数:- 函数原型:
void pop_back();
- 功能:从deque的末尾删除一个元素。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; myDeque.pop_back(); // 删除deque的最后一个元素 // 输出调整后的deque:1, 2, 3, 4 for (const auto& element : myDeque) { std::cout << element << " "; } std::cout << std::endl; return 0; }
- 函数原型:
3.5、deque的数据存取
C++ deque(双端队列)提供了一些函数来进行数据的存取操作。
-
at()
函数:- 函数原型:
T& at(size_t pos);
- 功能:返回指定位置(pos)的元素的引用,并进行边界检查。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 访问第三个元素(下标为2) int element = myDeque.at(2); std::cout << "Element at index 2: " << element << std::endl; return 0; }
- 函数原型:
-
operator[]
运算符重载:- 函数原型:
T& operator[](size_t pos);
- 功能:返回指定位置(pos)的元素的引用,不进行边界检查。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 访问第四个元素(下标为3) int element = myDeque[3]; std::cout << "Element at index 3: " << element << std::endl; return 0; }
- 函数原型:
-
front()
函数:- 函数原型:
T& front();
- 功能:返回deque的第一个元素的引用。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 访问第一个元素 int firstElement = myDeque.front(); std::cout << "First element: " << firstElement << std::endl; return 0; }
- 函数原型:
-
back()
函数:- 函数原型:
T& back();
- 功能:返回deque的最后一个元素的引用。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 访问最后一个元素 int lastElement = myDeque.back(); std::cout << "Last element: " << lastElement << std::endl; return 0; }
- 函数原型:
通过这些函数可以访问deque中指定位置的元素,或者获取deque的第一个和最后一个元素的值。请注意,在使用下标运算符 []
访问元素时,不进行边界检查,因此确保在合法的索引范围内进行访问,以避免访问超出deque的边界导致未定义行为。
3.6、deque的插入操作
C++ deque的插入操作函数insert()
提供了多种重载形式,可以在指定位置插入一个或多个元素。
-
insert()
函数(单个元素插入):- 函数原型:
iterator insert(iterator pos, const T& value);
- 功能:在位置
pos
之前插入单个元素value
,返回一个指向新插入元素的迭代器。 - 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 4, 5}; // 在第三个位置之前插入元素 auto it = myDeque.insert(myDeque.begin() + 2, 3); // 打印插入后的deque for (const auto& element : myDeque) { std::cout << element << " "; } return 0; }
- 函数原型:
-
insert()
函数(多个元素插入):- 函数原型:
iterator insert(iterator pos, InputIt first, InputIt last);
- 功能:在位置
pos
之前插入来自范围[first, last)
的元素,返回一个指向第一个新插入元素的迭代器。 - 示例:
#include <iostream> #include <deque> #include <vector> int main() { std::deque<int> myDeque {1, 2, 5}; std::vector<int> values {3, 4}; // 在第三个位置之前插入两个元素 auto it = myDeque.insert(myDeque.begin() + 2, values.begin(), values.end()); // 打印插入后的deque for (const auto& element : myDeque) { std::cout << element << " "; } return 0; }
- 函数原型:
通过这些insert()
函数的不同形式,可以在deque的指定位置插入一个或多个元素。使用单个元素插入形式可以方便地插入一个元素,而使用多个元素插入形式可以直接将一个范围内的元素插入到deque中。无论是单个元素还是多个元素插入,insert()
都返回一个指向新插入元素的迭代器,方便对插入结果进行进一步操作。
3.7、deque的删除操作
C++ deque的删除操作函数包括clear()
和erase()
,它们可以用于删除deque中的元素。
-
clear()
函数:- 函数原型:
void clear();
- 功能:清空deque中的所有元素,使其变为空deque。
- 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 清空deque myDeque.clear(); // 打印清空后的deque大小 std::cout << "Size of deque after clearing: " << myDeque.size() << std::endl; return 0; }
- 函数原型:
-
erase()
函数(单个元素删除):- 函数原型:
iterator erase(iterator pos);
- 功能:删除位置
pos
处的元素,并返回指向被删除元素之后的元素的迭代器。 - 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 删除第三个位置处的元素 auto it = myDeque.erase(myDeque.begin() + 2); // 打印删除后的deque for (const auto& element : myDeque) { std::cout << element << " "; } return 0; }
- 函数原型:
-
erase()
函数(范围删除):- 函数原型:
iterator erase(iterator first, iterator last);
- 功能:删除从位置
first
到last
之间的元素(不包括last
),并返回指向被删除元素之后的元素的迭代器。 - 示例:
#include <iostream> #include <deque> int main() { std::deque<int> myDeque {1, 2, 3, 4, 5}; // 删除第二个位置到第四个位置之间的元素 auto it = myDeque.erase(myDeque.begin() + 1, myDeque.begin() + 4); // 打印删除后的deque for (const auto& element : myDeque) { std::cout << element << " "; } return 0; }
- 函数原型:
通过使用clear()
函数,可以快速清空整个deque。而使用erase()
函数,可以删除一个或多个元素,无论是单个元素还是范围内的元素。在删除操作完成后,这些函数都提供了一个返回值,指示了删除后的新迭代器位置,方便对结果进行进一步操作。
四、deque容器在竞技类中的应用案例
假设有5名选手:选手ABCDE,10个评委分别对每一名选手打分,去除最高分,去除评委中最低分,取平均分。
代码示例:
#include <iostream>
#include <deque>
#include <algorithm> // 用于排序
int main() {
// 创建选手名单和评委打分的deque
std::deque<char> players {'A', 'B', 'C', 'D', 'E'};
std::deque<int> scores;
// 让每个评委为每位选手打分,共10个评委
for (int i = 0; i < 10; i++) {
// 在1到10之间生成随机得分(假设当前选手得分均为整数)
int score = rand() % 10 + 1;
scores.push_back(score);
}
// 对评委得分进行排序
std::sort(scores.begin(), scores.end());
// 去除最高分和最低分
scores.pop_back(); // 去除最高分
scores.pop_front(); // 去除最低分
// 计算平均分
double average = 0.0;
for (const auto& score : scores) {
average += score;
}
average /= scores.size();
// 打印结果
std::cout << "选手:" << players.front() << std::endl;
std::cout << "平均分:" << average << std::endl;
return 0;
}
总结
在本文中,深入探讨了C++标准模板库(STL)中的deque容器,并展示了它的强大功能。deque是一个双端队列,可以在两端高效地进行元素的插入和删除操作。
首先,介绍了deque的基本特点和使用方法。与vector相比,deque具有更好的插入和删除性能,因为它采用了分段连续存储的方式。这使得在队列的前端和后端执行插入和删除操作效率都很高,且不会导致内存重新分配。
其次,讨论了deque的迭代器和遍历方式。deque提供了双向迭代器,可以方便地在容器的任意位置进行元素的访问和修改。我们还展示了如何使用范围-for循环来遍历deque中的元素,并通过迭代器的逆序遍历实现了从尾部到头部的遍历。
接下来,着重介绍了deque的一些高级功能。例如,使用push_front()
和push_back()
函数可以在deque的前端和后端插入元素,而pop_front()
和pop_back()
函数则分别删除deque的第一个和最后一个元素。我们还展示了如何使用emplace()
和emplace_back()
函数来进行原地构造,并介绍了与deque相关的排序算法。
此外,由于deque的元素可能被分散存储在多个内存块中,为了减少指针引起的额外开销,可以使用指定一个特定的缓冲区大小,并调用shrink_to_fit()
函数来优化存储空间。
最后,通过一个实际案例展示了deque的应用场景。以选手打分为例,我们使用deque容器实现了对选手的评分汇总,并通过去除最高分和最低分来计算平均分。这个案例不仅演示了deque的双端操作特性,还展示了它在实际问题中的灵活运用。