栈和队列和优先队列
概述
学到这里对于容器的基础内容都是大概掌握了的,所以直接讲重点
栈(Stack)
栈是一种后进先出(LIFO, Last In First Out)的数据结构,类比为一个容器,你只能从容器的顶部(栈顶)添加或移除元素,不能在中间或底部进行操作。栈的基本操作包括:
- 压入(Push):将元素放入栈顶。
- 弹出(Pop):从栈顶移除元素。
- 查看栈顶元素(Top):查看但不移除栈顶元素。
- 判空(Empty):判断栈是否为空。
- 大小(Size):获取栈中元素的个数。
栈常用于需要"后进先出"操作的场景,比如函数调用的执行过程(存储局部变量和调用信息)、表达式求值(处理运算符优先级)、浏览器的返回按钮(记录访问历史)等。
队列(Queue)
队列是一种先进先出(FIFO, First In First Out)的数据结构,类比为排队,你只能从队列的一端(队尾)添加元素,从另一端(队首)移除元素。队列的基本操作包括:
- 入队(Enqueue):将元素添加到队列的末尾。
- 出队(Dequeue):从队列的头部移除元素。
- 查看队首元素(Front):查看但不移除队首元素。
- 查看队尾元素(Back):查看但不移除队尾元素。
- 判空(Empty):判断队列是否为空。
- 大小(Size):获取队列中元素的个数。
队列常用于需要"先进先出"操作的场景,比如任务调度(先到先服务)、缓冲管理(网络数据包传输)、打印队列(打印机任务管理)等。
优先队列(Priority Queue)
优先队列是一种根据元素的优先级来确定出队顺序的队列,而不是严格的先进先出。具体来说,每次出队操作都会返回具有最高(或最低)优先级的元素。优先队列的特点是:
元素之间有优先级顺序,高优先级的元素先出队。
通常使用堆(Heap)这种数据结构来实现,因为堆能够高效地支持插入和删除操作,并保持元素的部分有序性。
基本操作包括插入元素、获取顶部元素(具有最高优先级的元素)、删除顶部元素等。
优先队列常用于需要动态维护一组数据中的优先级顺序的场景,比如任务调度(优先级高的任务优先执行)、事件模拟(按照发生时间的优先级处理事件)等。
std::堆栈
template <class T, class Container = deque > class stack;
后进先出堆栈
堆栈是一种容器适配器,专门设计用于在后进先出环境(后进先出)下运行,其中元素仅从容器的一端插入和提取。
堆栈S 作为容器适配器实现,这些适配器是使用特定容器类的封装对象作为其底层容器的类,提供一组特定的成员函数来访问其元素。元素从特定容器的"背面"推出/弹出,该容器称为堆栈的顶部。
底层容器可以是任何标准容器类模板,也可以是其他一些专门设计的容器类。容器应支持以下操作:
empty
size
back
push_back
pop_back
标准容器类,并满足这些要求。默认情况下,如果未为特定类实例化指定容器类,则使用标准容器。
核心函数和操作
构造函数和析构函数
- stack:创建一个空栈,其中 T 是存储的数据类型。
- stack(const stack& other):复制构造函数,用于复制另一个栈的内容。
成员函数
- push(const T& value):将元素压入栈顶。
- pop():从栈顶移除元素,不返回任何值。
- top():返回栈顶元素的引用,但不将其从栈中移除。
- empty():检查栈是否为空,返回 true 或 false。
- size():返回栈中元素的数量。
示例
cpp
#include <iostream>
#include <stack>
int main() {
std::stack<int> s;
// Pushing elements onto the stack
s.push(10);
s.push(20);
s.push(30);
// Checking if stack is empty
if (!s.empty()) {
std::cout << "Stack size: " << s.size() << std::endl;
// Accessing top element
std::cout << "Top element: " << s.top() << std::endl;
// Popping elements
s.pop();
std::cout << "Popped top element." << std::endl;
// Accessing top element after popping
std::cout << "Top element now: " << s.top() << std::endl;
}
return 0;
}
注意事项
访问栈顶元素:使用 top() 函数之前,需要确保栈非空,否则会导致未定义行为。
移除栈顶元素:使用 pop() 函数会移除栈顶元素,但不返回该元素的值。
复制栈:使用复制构造函数和赋值操作符时,会复制整个栈的内容,包括元素顺序。
空栈访问:尝试在空栈上调用 top() 或 pop() 会导致运行时错误(未定义行为),因此在使用这些操作前应当检查栈是否为空。
std::队列
template <class T, class Container = deque > class queue;
FIFO队列
队列S 是一种容器适配器,专门设计用于在 FIFO 上下文(先进先出)中运行,其中元素插入容器的一端并从另一端提取。
队列 S 作为容器适配器实现,这些适配器是使用特定容器类的封装对象作为其底层容器的类,提供一组特定的成员函数来访问其元素。元素被推入特定容器的"后部",并从其"前部"弹出。
底层容器可以是标准容器类模板之一,也可以是其他一些专门设计的容器类。此基础容器应至少支持以下操作:
empty
size
front
back
push_back
pop_front
标准容器类别,并满足这些要求。默认情况下,如果未为特定类实例化指定容器类,则使用标准容器。
核心函数和操作
构造函数和析构函数
- queue:创建一个空队列,其中 T 是存储的数据类型。
- queue(const queue& other):复制构造函数,用于复制另一个队列的内容。
成员函数
- push(const T& value):将元素插入队列的末尾。
- pop():从队列的开头移除元素,不返回任何值。
- front():返回队列的第一个元素的引用,但不将其从队列中移除。
- back():返回队列的最后一个元素的引用,但不将其从队列中移除。
- empty():检查队列是否为空,返回 true 或 false。
- size():返回队列中元素的数量。
示例
cpp
#include <iostream>
#include <queue>
int main() {
std::queue<int> q;
// Pushing elements into the queue
q.push(10);
q.push(20);
q.push(30);
// Checking if queue is empty
if (!q.empty()) {
std::cout << "Queue size: " << q.size() << std::endl;
// Accessing front and back elements
std::cout << "Front element: " << q.front() << std::endl;
std::cout << "Back element: " << q.back() << std::endl;
// Popping elements
q.pop();
std::cout << "Popped front element." << std::endl;
// Accessing front element after popping
std::cout << "Front element now: " << q.front() << std::endl;
}
return 0;
}
注意事项
访问队列元素:使用 front() 和 back() 函数之前,需要确保队列非空,否则会导致未定义行为。
移除队列元素:使用 pop() 函数会移除队列的第一个元素,但不返回该元素的值。
复制队列:使用复制构造函数和赋值操作符时,会复制整个队列的内容,包括元素顺序。
空队列访问:尝试在空队列上调用 front()、back() 或 pop() 会导致运行时错误(未定义行为),因此在使用这些操作前应当检查队列是否为空。
std::优先队列
底层实现原理
在实现优先队列时,通常使用以下两种主要数据结构:
基于堆(Heap)的实现:
堆是一种特殊的二叉树,通常实现为数组。
最大堆(Max Heap):父节点的值总是大于或等于任何一个子节点的值。
最小堆(Min Heap):父节点的值总是小于或等于任何一个子节点的值。
在最大堆中,根节点是优先级最高的元素;在最小堆中,根节点是优先级最低的元素。
平衡二叉搜索树的实现:
使用平衡二叉搜索树(如红黑树)来维护有序序列,以支持快速的插入、删除和查找操作。
这种实现保证了元素能够按照优先级有序地存储和访问。
效率分析
插入操作:
基于堆的优先队列中,插入操作的时间复杂度为 O(log n),其中 n 是当前队列中元素的数量。这是因为插入新元素后,需要调整堆以恢复堆的性质。
平衡二叉搜索树实现的优先队列中,插入操作的时间复杂度为 O(log n),其中 n 是当前队列中元素的数量。这是因为需要保持树的平衡特性。
删除操作:
删除操作是指移除优先队列中优先级最高的元素。在基于堆的实现中,删除操作的时间复杂度为 O(log n),因为移除根节点后,需要进行堆的重排。
平衡二叉搜索树实现的优先队列中,删除操作的时间复杂度为 O(log n),因为需要保持树的平衡。
获取最高优先级元素:
获取优先队列中优先级最高的元素(即队头元素)的时间复杂度为 O(1)。这是因为在堆中,根节点始终是最大或最小值;在平衡二叉搜索树中,最小或最大元素也可以通过一些常数时间的操作得到。
deque双端队列
deque(双端队列)是C++标准模板库(STL)中的一种容器,它结合了向量(vector)和链表(list)的优点,支持在两端高效地进行元素的插入和删除操作。下面是关于 deque 容器的原理和效率的详细解释:
原理
deque 的名称来源于 "double-ended queue",即双端队列。它通过一系列的块(chunks)来存储元素,每个块内部通常是一个固定大小的数组,这个数组可以容纳多个元素。deque 的关键设计包括:
块结构:
deque 内部通常由多个块组成,每个块是一个固定大小的数组(或者链表节点)。
每个块存储元素,并且每个块具有前驱和后继块的指针,形成了一个双向链表结构。
指针管理:
deque 维护了指向第一个块和最后一个块的指针,使得在队列的头部和尾部进行快速插入和删除操作成为可能。
对于头部和尾部的插入和删除操作,通过调整指针和在必要时创建或销毁块来维护块的结构。
元素访问:
deque 允许使用迭代器访问元素,迭代器支持双向遍历,并且支持随机访问(通过双向迭代器和块索引的组合实现)。
内存分配:
deque 内部使用动态内存分配来管理块的创建和销毁。这种方式避免了频繁的内存重分配,提高了插入和删除操作的效率。
效率
deque 提供了以下操作的平均时间复杂度:
头部插入和删除(push_front, pop_front):O(1)
尾部插入和删除(push_back, pop_back):O(1)
随机访问(通过迭代器或下标访问):O(1)
相比于 vector 和 list,deque 的主要优势在于
:
头部操作的高效性:与 vector 相比,deque 的头部插入和删除操作更为高效,因为 vector 需要移动大量元素。
尾部操作的高效性:与 list 相比,deque 在尾部插入和删除操作上的效率更高,因为 list 的每个元素都需要一个额外的指针。
示例代码
cpp
#include <deque>
#include <iostream>
int main() {
std::deque<int> dq;
dq.push_back(1); // 尾部插入
dq.push_front(2); // 头部插入
std::cout << dq.front() << " " << dq.back() << std::endl; // 输出:2 1
dq.pop_back(); // 尾部删除
dq.pop_front(); // 头部删除
return 0;
}