目录
定义
deque
,即双端队列(double-ended queue),是一种具有队列和栈的性质的数据结构。它允许你在队列的两端进行元素的插入和删除操作。在C++的STL(Standard Template Library)中,deque
是一个模板类,可以存储任何类型的数据。
主要API
-
构造函数
deque<T>()
: 创建一个空的双端队列。deque<T>(beg, end)
: 创建一个双端队列,并用[beg, end)区间中的元素初始化。deque<T>(n, elem)
: 创建一个包含n个elem元素的双端队列。deque<T>(const deque<T>& deq)
: 拷贝构造函数,创建一个与给定deque相同的双端队列。
-
赋值操作
assign(beg, end)
: 用[beg, end)区间中的元素替换双端队列中的元素。assign(n, elem)
: 用n个elem元素替换双端队列中的元素。
-
访问元素
front()
: 返回双端队列的第一个元素。back()
: 返回双端队列的最后一个元素。
-
插入操作
insert(pos, elem)
: 在pos位置插入一个elem元素的拷贝,并返回新数据的位置。insert(pos, n, elem)
: 在pos位置插入n个elem数据,无返回值。
-
其他操作
push_front(elem)
: 在双端队列的前端插入一个元素。push_back(elem)
: 在双端队列的后端插入一个元素。pop_front()
: 删除双端队列的前端元素。pop_back()
: 删除双端队列的后端元素。
示例详解
#include <iostream>
#include <deque>
int main() {
// 创建一个空的双端队列
std::deque<int> myDeque;
// 在双端队列的前端和后端插入元素
myDeque.push_front(1);
myDeque.push_back(2);
// 访问并输出双端队列的第一个和最后一个元素
std::cout << "Front: " << myDeque.front() << std::endl;
std::cout << "Back: " << myDeque.back() << std::endl;
// 在双端队列的前端插入一个元素
myDeque.push_front(0);
// 输出双端队列的所有元素
for (int elem : myDeque) {
std::cout << elem << " ";
}
std::cout << std::endl;
// 删除双端队列的前端元素
myDeque.pop_front();
// 再次输出双端队列的所有元素
for (int elem : myDeque) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
这个示例创建了一个空的双端队列,然后在其前端和后端插入元素,并展示了如何访问和删除这些元素。
deque 相对于别的容器的优势
std::deque
(双端队列)相对于其他C++标准库容器(如std::vector
和std::list
)具有一些独特的优势:
- 高效的头部和尾部操作:
std::deque
在其头部和尾部进行插入和删除操作的时间复杂度为O(1),即常数时间。这是std::deque
相对于其他容器的关键优势之一。这使得std::deque
非常适合作为栈(使用std::deque
作为底层容器的std::stack
)和队列(使用std::deque
作为底层容器的std::queue
)的实现。 - 支持随机访问:虽然
std::deque
不是连续存储的,但它仍然支持随机访问,可以通过下标运算符([]
)或at
函数来访问任何位置的元素。这使得std::deque
在需要随机访问的同时,仍然能够保持头部和尾部操作的高效性。 - 中间插入和删除的效率:虽然
std::deque
在中间位置进行插入和删除操作的时间复杂度为O(n),但在实践中,由于std::deque
的内部实现(通常是一个分段连续的内存块链表),这些操作通常比std::vector
的中间插入和删除操作要快。 - 动态调整大小:
std::deque
可以动态地增长和缩小,而不需要像std::vector
那样在需要时重新分配和复制内存。这使得std::deque
在处理动态大小的数据集时更加灵活。 - 空间效率:
std::deque
通常比std::vector
和std::list
更加空间高效,因为它只在需要时分配内存,并且不会浪费大量空间来维护连续的存储。
deque 相对于别的容器的劣势
std::deque
(双端队列)虽然具有在头部和尾部高效添加和删除元素的优点,但它也有相对于其他容器的一些缺点:
-
空间碎片化:
std::deque
通常通过分段连续的内存块来实现,这意味着它可能会导致空间碎片化。尽管每个内存块内部是连续的,但不同内存块之间可能不是连续的。这可能会影响到缓存局部性,并可能增加内存管理的复杂性。 -
不支持随机访问迭代器:虽然
std::deque
支持通过下标运算符([]
)或at
函数进行随机访问,但它的迭代器不是随机访问迭代器。这意味着你不能使用所有适用于随机访问迭代器的算法和操作,比如std::advance
来前进或后退多个位置。 -
中间插入和删除效率较低:虽然
std::deque
在头部和尾部的插入和删除操作是高效的,但在中间位置进行这些操作的效率则较低。这是因为需要移动多个元素来保持连续性,时间复杂度为O(n)。 -
不支持
reserve
和capacity
:std::deque
没有提供reserve
和capacity
成员函数,这意味着你不能预先分配固定大小的内存空间。这可能会导致内存重新分配和元素复制的开销,尤其是在频繁添加元素时。 -
额外的空间开销:由于
std::deque
使用分段连续的内存块,每个块可能包含一些额外的空间用于管理(如指向下一个块的指针)。这可能会导致相对于std::vector
等连续存储容器额外的空间开销。 -
限制性的迭代器失效:在
std::deque
中,只有插入和删除操作涉及的迭代器会失效,而不是像std::vector
那样可能导致所有迭代器失效。然而,这仍然可能会影响到算法的正确性和性能,需要特别注意。
综上所述,选择使用std::deque
还是其他容器取决于具体的应用场景和性能要求。在某些需要高效头部和尾部操作的情况下,std::deque
可能是最佳选择;而在其他需要连续存储、随机访问或高效中间操作的情况下,其他容器可能更合适。
deque 生活应用示例
双端队列(deque)是一种可以在序列头部和尾部进行插入和删除操作的数据结构,具有常数时间复杂度的特性。在实际生活中,deque 可以用于许多场景。以下是一些 deque 的生活应用示例:
- 撤销/重做功能:在许多文本编辑器或图形设计工具中,用户可能需要撤销或重做之前的操作。deque 可以用于存储这些操作的历史,以便在需要时能够快速进行撤销或重做。deque 的双端特性使得在头部(撤销操作)和尾部(重做操作)添加和删除元素变得非常高效。
- 滑动窗口问题:在计算机科学中,滑动窗口是一种常见的技术,用于处理一系列数据,如数组或列表。deque 可以用于实现滑动窗口,以便在保持窗口大小不变的同时,高效地添加和删除元素。例如,在处理股票市场的移动平均线时,可以使用 deque 来存储窗口内的数据,并计算平均值。
- 任务调度:在操作系统中,任务调度器需要管理一组等待执行的任务。deque 可以用于实现优先级队列或任务队列,以便按照优先级或提交顺序执行任务。通过将新任务添加到队列的尾部,并从头部删除任务来执行,deque 可以提供高效的插入和删除操作。
- 浏览器的前进/后退功能:在浏览器中,用户可能需要通过点击前进或后退按钮来导航到之前或之后的页面。deque 可以用于存储用户的浏览历史,以便在需要时能够快速进行前进或后退操作。通过将新页面添加到 deque 的尾部(或头部),并从相应的端点删除页面,可以实现高效的导航功能。
这些示例只是 deque 在生活中的应用之一,实际上 deque 可以在许多需要高效地在序列头部和尾部进行插入和删除操作的场景中使用。
应用示例
下面我将使用 C++ 代码来演示一个简单的 std::deque
应用示例,即实现一个支持撤销和重做功能的简单文本编辑器。
#include <iostream>
#include <deque>
#include <string>
class SimpleTextEditor {
private:
std::deque<std::string> actions; // 用于存储编辑历史的双端队列
std::string currentText; // 当前文本内容
public:
// 输入文本
void typeText(const std::string& text) {
currentText += text;
actions.push_back(text); // 将输入的文本添加到历史队列的尾部
}
// 撤销操作
void undo() {
if (!actions.empty()) {
std::string lastAction = actions.back();
actions.pop_back(); // 从队列尾部删除最后一个操作
currentText.pop_back(lastAction.size()); // 回退文本内容
}
}
// 重做操作
void redo() {
if (!actions.empty()) {
std::string redoAction = actions.front();
actions.pop_front(); // 从队列头部删除第一个操作
currentText += redoAction; // 重新应用文本内容
actions.push_front(redoAction); // 将该操作重新放入队列头部
}
}
// 显示当前文本内容
void displayText() const {
std::cout << currentText << std::endl;
}
// 显示编辑历史
void displayHistory() const {
std::cout << "Undo history:" << std::endl;
for (const auto& action : actions) {
std::cout << action << std::endl;
}
}
};
int main() {
SimpleTextEditor editor;
// 用户输入一些文本
editor.typeText("Hello, ");
editor.typeText("world!");
editor.displayText(); // 输出: Hello, world!
editor.displayHistory(); // 显示编辑历史
// 用户撤销一个操作
editor.undo();
editor.displayText(); // 输出: Hello,
editor.displayHistory(); // 显示编辑历史
// 用户重做一个操作
editor.redo();
editor.displayText(); // 输出: Hello, world!
editor.displayHistory(); // 显示编辑历史
return 0;
}
在这个示例中,SimpleTextEditor
类使用一个 std::deque
来存储用户的编辑操作历史。每当用户输入文本时,这些文本被添加到历史队列的尾部。当用户执行撤销操作时,从队列尾部删除最后一个操作,并回退文本内容。当用户执行重做操作时,将队列头部的操作重新应用到文本中,并将其重新放回队列头部。
这个示例演示了 std::deque
如何在实际应用中发挥其高效在头部和尾部添加和删除元素的特性。当然,这个例子非常简化,真实的文本编辑器会有更多的功能和更复杂的撤销/重做机制。