C++,STL,queue 队列:FIFO 队列与单调队列的深挖与扩展

本文深入讲解了队列的基础知识、二叉树的层次遍历、循环队列的设计及单调队列的应用,尤其针对队列的不同应用场景提供了详尽的代码实例。

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值