1.deque的使用
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和 删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
构造和析构
std::deque<T>
:创建一个存储类型为T
的元素的双端队列。std::deque(const std::deque<T>& other)
:复制构造函数,用另一个双端队列初始化当前队列。~std::deque()
:析构函数,释放内存。
示例:
#include <iostream>
#include <deque>
int main() {
// 创建一个存储整数的双端队列
std::deque<int> myDeque;
// 在队尾插入元素
myDeque.push_back(10);
myDeque.push_back(20);
myDeque.push_back(30);
// 复制构造一个新的双端队列
std::deque<int> anotherDeque = myDeque;
// 输出原始队列的内容
std::cout << "Original deque content:";
for (const int& value : myDeque) {
std::cout << " " << value;
}
std::cout << std::endl;
// 输出复制构造的队列的内容
std::cout << "Copied deque content:";
for (const int& value : anotherDeque) {
std::cout << " " << value;
}
std::cout << std::endl;
// 注意:析构函数会在作用域结束时自动调用,释放内存
return 0;
}
容量相关
size()
:返回队列中元素的数量。empty()
:检查队列是否为空。
示例:
#include <deque>
#include <iostream>
int main() {
std::deque<int> myDeque = {10, 20, 30, 40};
std::cout << myDeque.size() << std::endl; //4
std::cout << myDeque.empty() << std::endl;//0
return 0;
}
resize(new_size)
:调整队列的大小。
void resize(size_type count);
void resize(size_type count, const value_type& value);
count
:新的队列大小。value
:在增加队列大小时用于填充新元素的默认值。
功能:
- 如果
count
小于当前队列的大小,resize
会移除尾部的元素,使队列的大小变为count
。 - 如果
count
大于当前队列的大小,resize
会在尾部插入足够数量的默认值为value
的元素,使队列的大小变为count
。
示例:
#include <iostream>
#include <deque>
int main() {
std::deque<int> myDeque = {10, 20, 30};
// 调整队列大小为 5,填充默认值 0
myDeque.resize(5);
for (const int& value : myDeque) {
std::cout << " " << value; // 10 20 30 0 0
}
std::cout << std::endl;
// 调整队列大小为 3
myDeque.resize(3);
for (const int& value : myDeque) {
std::cout << " " << value; // 10 20 30
}
std::cout << std::endl;
return 0;
}
元素访问
at(index)
:函数返回指定索引处的元素,并在访问之前进行范围检查。如果索引超出了双端队列的有效范围,将引发std::out_of_range
异常。operator[](index)
:运算符返回指定索引处的元素,但不进行范围检查。如果索引超出了有效范围,行为将是未定义的。front()
:返回第一个元素的引用。back()
:返回最后一个元素的引用。
示例:
#include <deque>
#include <iostream>
int main() {
// 创建一个存储字符串的双端队列
std::deque<std::string> myDeque;
// 在队尾插入元素
myDeque.push_back("Alice");
myDeque.push_back("Bob");
myDeque.push_back("Charlie");
// 使用 at(index) 进行范围安全的访问
try {
std::string value = myDeque.at(1);// 索引1处的元素是 "Bob"
std::cout << value << std::endl; //Bob
} catch (const std::out_of_range &e) {
std::cout << "Index is out of range." << std::endl;
}
// 使用 operator[](index) 进行不安全的访问
std::string unsafeValue = myDeque[2];// 索引2处的元素是 "Charlie"
std::cout << "不安全: " << unsafeValue << std::endl;
// 使用 front() 访问第一个元素
std::string firstElement = myDeque.front();
std::cout << firstElement << std::endl;
// 使用 back() 访问最后一个元素
std::string lastElement = myDeque.back(); //Alice
std::cout << lastElement << std::endl; //Charlie
return 0;
}
修改操作
push_back(value)
:在队尾插入一个元素。push_front(value)
:在队首插入一个元素。pop_back()
:移除队尾的元素。pop_front()
:移除队首的元素。insert(pos, value)
:在指定位置插入一个元素。erase(pos)
:移除指定位置的元素。clear()
:移除所有元素。
示例:
#include <deque>
#include <iostream>
int main() {
std::deque<int> myDeque;
// 在队尾插入元素
myDeque.push_back(10);
myDeque.push_back(20);
// 在队首插入元素
myDeque.push_front(5);
// 输出队列内容
for (const int &value: myDeque) {
std::cout << value << " ";//5 10 20
}
std::cout << std::endl;
// 移除队尾的元素
myDeque.pop_back();
// 移除队首的元素
myDeque.pop_front();
// 输出队列内容
for (const int &value: myDeque) {
std::cout << value << " ";//10
}
std::cout << std::endl;
// 在指定位置插入元素
std::deque<int>::iterator it = myDeque.insert(myDeque.begin() + 1, 15);
// 输出队列内容
for (const int &value: myDeque) {
std::cout << value << " ";//10 15
}
std::cout << std::endl;
// 移除指定位置的元素
myDeque.erase(it);
// 输出队列内容
for (const int &value: myDeque) {
std::cout << value << " ";//10
}
std::cout << std::endl;
// 清空队列
myDeque.clear();
if (myDeque.empty()) {
std::cout << "Deque is empty.";
} else {
std::cout << "Deque is not empty.";
}
std::cout << std::endl;
return 0;
}
迭代器
begin()
和end()
:返回指向第一个元素和尾后元素的迭代器。
示例:
#include <iostream>
#include <deque>
int main() {
std::deque<int> myDeque = {10, 20, 30, 40};
for (std::deque<int>::iterator it = myDeque.begin(); it != myDeque.end(); ++it) {
std::cout << " " << *it; //10 20 30 40
}
std::cout << std::endl;
return 0;
}
rbegin()
和rend()
:返回指向最后一个元素和首元素前一个位置的逆向迭代器。
示例:
#include <iostream>
#include <deque>
int main() {
std::deque<int> myDeque = {10, 20, 30, 40};
for (std::deque<int>::reverse_iterator rit = myDeque.rbegin(); rit != myDeque.rend(); ++rit) {
std::cout << " " << *rit; //40 30 20 10
}
std::cout << std::endl;
return 0;
}
2.deque的原理介绍
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
3.deque的优缺点
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。 但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构 时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
4.为什么选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。