1、队列的基础知识
和 stack 栈容器适配器不同,queue 容器适配器有 2 个开口,其中一个开口专门用来输入数据,另一个专门用来输出数据。如图所示:
queue 容器适配器以模板类 queue<T,Container=deque<T>>(其中 T 为存储元素的类型,Container 表示底层容器的类型)的形式位于<queue>头文件中,并定义在 std 命名空间里。因此,在创建该容器之前,程序中应包含以下 2 行代码:
#include <queue>
using namespace std;
1) 创建一个空的 queue 容器适配器,其底层使用的基础容器选择默认的 deque 容器:
std::queue<int> values;
通过此行代码,就可以成功创建一个可存储 int 类型元素,底层采用 deque 容器的 queue 容器适配器。
2) 当然,也可以手动指定 queue 容器适配器底层采用的基础容器类型。
queue 容器适配器底层容器可以选择 deque 和 list。作为 queue 容器适配器的基础容器,其必须提供 front()、back()、push_back()、pop_front()、empty() 和 size() 这几个成员函数,符合条件的序列式容器仅有 deque 和 list。
例如,下面创建了一个使用 list 容器作为基础容器的空 queue 容器适配器:
std::queue<int, std::list<int>> values;
注意,在手动指定基础容器的类型时,其存储的数据类型必须和 queue 容器适配器存储的元素类型保持一致。
3) 可以用基础容器来初始化 queue 容器适配器,只要该容器类型和 queue 底层使用的基础容器类型相同即可。例如:
std::deque<int> values{1,2,3};
std::queue<int> my_queue(values);
由于 my_queue 底层采用的是 deque 容器,和 values 类型一致,且存储的也都是 int 类型元素,因此可以用 values 对 my_queue 进行初始化。
4) 还可以直接通过 queue 容器适配器来初始化另一个 queue 容器适配器,只要它们存储的元素类型以及底层采用的基础容器类型相同即可。
例如:
std::deque<int> values{1,2,3};
std::queue<int> my_queue1(values);
std::queue<int> my_queue(my_queue1);//或者使用 std::queue<int> my_queue = my_queue1;
注意,和使用基础容器不同,使用 queue 适配器给另一个 queue 进行初始化时,有 2 种方式,使用哪一种都可以。
值得一提的是,第 3、4 种初始化方法中 my_queue 容器适配器的数据是经过拷贝得来的,也就是说,操作 my_queue 容器适配器中的数据,并不会对 values 容器以及 my_queue1 容器适配器有任何影响;反过来也是如此。
综上,queue我理解的初始化有三种,第一种是声明一个空的容器,第二种用基础容器来初始化,第三种用queue容器初始化。(在vs里试验,初始化的时候,不能采用迭代器选择,只能全部赋值)
5)常用函数
push() //向队列尾部添加元素
pop() //从队列头删除元素
front() //返回队列头部元素
back() //返回队列尾部元素
size() //返回队列元素个数
empty() //判断队列是否为空,为空返回true
2、二叉树的层次遍历
二叉树的层次遍历,是广度优先搜素的一个经典例子,利用队列去解决问题。
二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层序遍历结果:[
[3],
[9,20],
[15,7]
]
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
//定义一个队列,用来存储每一层
queue<TreeNode*> queTree;
queTree.push(root);
//保存结果
vector<vector<int>> result;
while (!queTree.empty()) {
vector<int> path;
//path 用来存储每一层的数据,len 在此声明,因为后面的操作会改变队列元素个数
int len = queTree.size();
for (int i = 0; i < len; i++) {
//读取队列头,并且存入path,删除,将队列头的左子树右子树存入队列
TreeNode* cur = queTree.front();
queTree.pop();
path.push_back(cur->val);
if (cur->left) queTree.push(cur->left);
if (cur->right) queTree.push(cur->right);
}
result.push_back(path);
}
return result;
}
};

以上的题型,万变不离其宗,都可以用上面的方法,稍微做改变既可以。
3、循环队列
【题目】设计一个可以容纳 k 个元素的循环队列。需要实现以下接口:
class MyCircularQueue {
// 参数k表示这个循环队列最多只能容纳k个元素
public MyCircularQueue(int k);
// 将value放到队列中, 成功返回true
public boolean enQueue(int value);
// 删除队首元素,成功返回true
public boolean deQueue();
// 得到队首元素,如果为空,返回-1
public int Front();
// 得到队尾元素,如果队列为空,返回-1
public int Rear();
// 看一下循环队列是否为空
public boolean isEmpty();
// 看一下循环队列是否已放满k个元素
public boolean isFull();
}
循环队列的重点在于循环使用固定空间,难点在于控制好 front/rear 两个首尾指示器。一般常用的有两种实现:
【方法 1】只使用 k 个元素的空间,三个变量 front, rear, used 来控制循环队列的使用。
【方法 2】方法 1 利用 used 变量对满队列和空队列进行了区分。实际上,这种区分方式还有另外一种办法,使用 k+1 个元素的空间,两个变量 front, rear 来控制循环队列的使用。具体如下:在申请数组空间的时候,申请 k + 1 个空间; 在放满循环队列的时候,必须要保证 rear 与 front 之间有空隙。
其中需要注意的一个问题是,虽然是循环数组,但是用普通数组实现,在下标的移动上,要特别注意不要越界。以第一种方法为例:下标只能在 [0, k-1] 范围里面移动。以下 3 点需要你格外注意,正常情况下:
index = i 的后一个是 i + 1,前一个是 i - 1
index = k-1 的后一个就是 index = 0
index = 0 的前一个是 index = k-1
实际上,这三个式子都可以利用取模的技巧来统一处理:
-
index = i 的后一个 (i + 1) % capacity
-
index = i 的前一个(i - 1 + capacity) % capacity
注意:所有的循环数组下标的处理都需要按照这个取模方法来。
【方法 1】:
#include <vector>
using namespace std;
class MyCircularQueue {
// 已经使用的元素个数
int used = 0;
// 第一个元素所在位置
int front = 0;
// rear是enQueue可在存放的位置
// 注意开闭原则
// [front, rear)
int rear = 0;
// 循环队列最多可以存放的元素个数
int capacity = 0;
// 循环队列的存储空间
vector<int> a;
public:
MyCircularQueue(int k) {
// 初始化循环队列
capacity = k;
a.resize(k);
}
bool enQueue(int value) {
// 如果已经放满了
if (used == capacity) {
return false;
}
// 如果没有放满,那么a[rear]用来存放新进来的元素
a[rear] = value;
// rear注意取模
rear = (rear + 1) % capacity;
// 已经使用的空间
used++;
// 存放成功!
return true;
}
bool deQueue() {
// 如果是一个空队列,当然不能出队
if (used == 0) {
return false;
}
// 第一个元素取出
int ret = a[front];
// 注意取模
front = (front + 1) % capacity;
// 已经存放的元素减减
used--;
// 取出元素成功
return true;
}
int Front() {
// 如果为空,不能取出队首元素
if (used == 0) {
return -1;
}
// 取出队首元素
return a[front];
}
int Rear() {
// 如果为空,不能取出队尾元素
if (used == 0) {
return -1;
}
// 注意:这里不能使用rear - 1
// 需要取模
int tail = (rear - 1 + capacity) % capacity;
return a[tail];
}
// 队列是否为空
bool isEmpty() { return used == 0; }
// 队列是否满了
bool isFull() { return used == capacity; }
};
【方法 2】:
#include <vector>
using namespace std;
class MyCircularQueue {
// 队列的头部元素所在位置
int front = 0;
// 队列的尾巴
// 注意我们采用的是前开后闭原则
// [front, rear)
int rear = 0;
vector<int> a;
int capacity = 0;
public:
// 初始化队列,注意此时队列中元素个数为k+1
MyCircularQueue(int k) : capacity(k + 1) { a.resize(k + 1); }
bool enQueue(int value) {
// 如果已经满了,无法入队
if (isFull()) {
return false;
}
// 把元素放到rear位置
a[rear] = value;
// rear向后移动
rear = (rear + 1) % capacity;
return true;
}
bool deQueue() {
// 如果为空,无法出队
if (isEmpty()) {
return false;
}
// 出队之后,front要向前移
front = (front + 1) % capacity;
return true;
}
// 如果能取出第一个元素,取a[front];
int Front() { return isEmpty() ? -1 : a[front]; }
// 由于我们使用的是前开后闭原则
// [front, rear)
// 所以在取最后一个元素时,应该是
// (rear - 1 + capacity) % capacity;
int Rear() {
int tail = (rear - 1 + capacity) % capacity;
return isEmpty() ? -1 : a[tail];
}
// 队列是否为空
bool isEmpty() { return front == rear; }
// rear与front之间至少有一个空格
// 当rear指向这个最后的一个空格时,
// 队列就已经放满了!
bool isFull() { return (rear + 1) % capacity == front; }
};
4、单调队列 求窗口最值
单调队列要求:队列中的元素必须满足单调性,比如单调递增,或者单调整递减。那么在入队与出队的时候,就与普通的队列不一样了。
举个单调递减队列的例子,6 5 3 2 4 依次入队,过程如下:

当元素4入队时候,4 与 2比较,2小,2从队尾出去, 继续与3比, 3小, 3从队尾出去,继续与 5 比,5大,不用出队,4进队。最后 队列状态为 654。
此时的队列所用的容器,不再是传统所说的一个口进一个口出的队列,而是双端队列,两个口既可以出,也可以进。deque的知识可以参考deque 简单介绍。
例题1
给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。
输入:nums = [1,3,-1,-3,5,3], k = 3
输出:[3,3,5,5]

//从队列末尾入队
void Qpush(deque<int>& Que, int val) {
while(!Que.empty() && Que.back() < val) {
Que.pop_back();
}
Que.push_back(val);
}
//从队头出队
void Qpop(deque<int>& Que, int val) {
if (!Que.empty() && Que.front() == val) {
Que.pop_front();
}
}
vector<int> maxResult(vector<int>& nums, int k){
int len = nums.size();
deque<int> Mydeque;
vector<int> result;
for (int i = 0; i < len; i++) {
Qpush(Mydeque,nums[i]);
if (i < k - 1) continue;
result.push_back(Mydeque.front());
Qpop(Mydequenums[i - k + 1]);
}
return result;
}
本文深入讲解了队列的基础知识、二叉树的层次遍历、循环队列的设计及单调队列的应用,尤其针对队列的不同应用场景提供了详尽的代码实例。

850

被折叠的 条评论
为什么被折叠?



