5.栈与队列

5.栈与队列

基础知识

队列是先进先出,栈是先进后出。
在这里插入图片描述
C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。三个最为普遍的STL版本

  1. HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
  2. P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
  3. SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
    栈提供push pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator),不像是set 或者map 提供迭代器iterator来遍历所有元素。栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能).
    所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)
    在这里插入图片描述
    我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构(deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了)。
    以C++语言的数据结构为例子,设置std::vector,list为栈的底层实现。
std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

题目

1.使用std::stack实现std::queue功能

// 使用栈实现队列功能-stack基础接口.push,pop,top,
class MyQueue{
public:
    std::stack<int> Inst;
    std::stack<int> Oust;
    MyQueue(){};
    ~MyQueue(){};
    void push(int x){
        Inst.push(x);
    }
    // queue.pop会弹出头部的元素
    int pop(){
        if(Oust.empty()){
            while (!Inst.empty())
            {
                Oust.push(Inst.top());
                Inst.pop();
            }
        }
        int res = Oust.top();
        Oust.pop();
        return res;
    }
    // queue.peek会返回头部的元素,但不会弹出
    int peek(){
        int res = this->pop();
        Oust.push(res);
        return res;
    }
    bool empty(){
        if(Inst.empty() && Oust.empty()) return true;
        else return false;
    }
};

2.使用std::queue实现std::stack功能

// 使用队列实现堆栈-queue基础接口.push,pop,front,back(peek),empty
class MyStack{
public:
    MyStack(){};
    ~MyStack(){};
    std::queue<int> que1;
    std::queue<int> que2;
    
    void push(int x){
        que1.push(x);
    }

    int pop(){
        int res = que1.back();
        while (que1.size()>1)
        {
            que2.push(que1.front());
            que1.pop();
        }
        que1.pop();
        while (que2.size()>0)
        {
            que1.push(que2.front());
            que2.pop();
        }
        return res;
    }

    int top(){
        return que1.back();
    }

    bool empty(){
        if(que1.empty()) return true;
        else return false;
    }
};

3.判断有效括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。

判断括号是否有效,可以利用栈结构的特殊性,解决一些对称匹配的问题。从左向右遍历字符串,将遍历的到的左括号push进stack,在遍历到的右括号,将对应括号pop出stack,并与有括号比较是否匹配。分析会出现无效的括号的情况主要为分以下三种:

  1. 左括号多余了
  2. 有括号多余了
  3. 括号没有多余,但左右括号的类型没有匹配上
    其实也存在一种情况是先是右括号出现了,再是左括号出现,这种情况会被上述的2情况所区分出来,当stack为空时,字符串仍未遍历完,也就是出现了未匹配的有括号。

4.删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一
例如 输入:“abbaca” 输出:“ca”

思路:创建一个stack,可通过一次遍历S,每次遍历将S[i]stack.top()进行比较,若相同,则使stack.pop()弹出,若不同,则将stack.push[S[i])。最后再将stack里字符依次pop出来给std::string,得到一顺序相反的字符串。反向该字符串即可。


5.逆波兰表达式求值
逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。这与我们使用的中缀表达式不同。逆波兰表达式主要有以下两个优点:

  1. 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  2. 适合用栈操作运算:遇到数字则入栈遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中

根据 逆波兰表示法,求表达式的值。有效的运算符包括+ , - , * , /。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例:

  • 输入: [“2”, “1”, “+”, “3”, " * "]
  • 输出: 9
  • 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

题外话:我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了


6.滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

暴力求解法:遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。
对于滑动窗口,我们可以使用队列queue,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列返回i里面的最大值。
该如何返回队列返回i里面的最大值?需要对队列里的元素进行排序,因此需要队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。

例如:对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。当窗口向右移动时,动态窗口的front端需要弹出,若单调队列里的front端与此时动态窗口的front端的数值相等,则一同将单调队列里的front的数值弹出;动态窗口back端需要压入新的数值new,此时需要将单调队列的back端数值与新数值new比较,若back端数值小于new,则将back端数值弹出,直至大于new,此时再压入单调队列的back端——这样就保证了单调队列是单调递减的,而且front端是最大值。


7.前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:

  • 输入: nums = [1,1,1,2,2,3], k = 2
  • 输出: [1,2]

思路

  • 统计元素出现的频率——使用map来统计元素出现次数
  • 对频率进行排序——使用一种 容器适配器就是优先级队列
  • 找出前k个高频元素

优先级队列:其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。优先级队列内部元素是自动依照元素的权值排列

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)

堆:是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆
我们可以使用快排要将map转换为vector的结构,然后对整个数组进行排序,而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。
如果定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢,所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素
在这里插入图片描述
思考:为了建立小顶堆,需要使用left>right的,从而建立了一个递减的队列,但使用std::priority_queue::pop()把队尾的元素(即最小数值)弹出。这与优先级队列的定义正好反过来了,可能和优先级队列的源码实现有关(我没有仔细研究),我估计是底层实现上优先队列队首指向后面,队尾指向最前面的缘故

// 优先队列priority_queue优先级队列的权值函数
struct MyComparison{
    bool operator()(const std::pair<int, int>& lhs, const std::pair<int, int>& rhs) {
        return lhs.second > rhs.second;
    }
};

总结

  • 在栈与队列系列中,强调栈与队列的基础,也是很容易忽视的点。
  • 使用抽象程度越高的语言,越容易忽视其底层实现,而C++相对来说是比较接近底层的语言。
  • 使用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。
  • 通过括号匹配问题、字符串去重问题、逆波兰表达式问题来系统讲解了栈在系统中的应用,以及使用技巧。
  • 通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。

栈里面的元素在内存中是连续分布的么?这个问题有两个陷阱:
陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。
陷阱2:缺省情况下,默认底层容器是deque,那么deque在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。

在了解栈和队列后,之前题目使用队列实现栈功能中,其实只用一个队列就够了,一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。

栈在系统中的应用
如linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。假使一个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用。这在leetcode上也是一道题目,编号:71. 简化路径。

对称性数据匹配问题
这种带有对称性数据的匹配,需要先分析出有几种不匹配的情况。例如 判断有效符号,字符串去重

逆波兰表达式
这题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程

滑动窗口最大值问题
这题是十分经典的队列题目,主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。而保证队列的元素的排序就叫做单调队列——C++中没有直接支持单调队列,需要我们自己来一个单调队列。而且必须强调的一点是与优先级队列的区别,后者是对窗口里面的数进行排序。

求前K个高频元素
通过求前 K 个高频元素,引出另一种队列就是优先级队列。这是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。而且优先级队列内部元素是自动依照元素的权值排列,因此需要自己设计权值函数。

  • 32
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值