代码随想录刷题——6栈与队列

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

6.1 栈与队列理论基础

队列:先进先出
栈:先进后出

在这里插入图片描述

栈和队列是STL(C++标准库)里面的两个数据结构。

C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。

那么来介绍一下,三个最为普遍的STL版本:

HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。

P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。

SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。

6.1.1 栈

栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。

栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。

栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
在这里插入图片描述
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。

deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。

我们也可以指定vector为栈的底层实现,初始化语句如下:

std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈

栈是一种后进先出(Last in First out,LIFO)的数据类型。每次元素入栈时只能添加到栈顶,出栈时只能从栈顶元素出栈。C++中,使用栈需要包含头文件stack。C++中栈的基本操作如下:

push() :入栈。在栈顶添加一个元素,无返回值;
pop() :出栈。将栈顶元素删除(出队),无返回值;
top() :获得栈顶元素。此函数返回值为栈顶元素,常与pop()函数一起,先通过top()获得栈顶元素,然后将其从栈中删除;
size() :获得栈大小。此函数返回栈的大小,返回值也是“size_t”类型的数据,“size_t”是“unsigned int”的别名。
empty() :判断栈是否为空。此函数返回栈是否为空,返回值是bool类型。栈空:返回true;不空:返回false。

6.1.2 队列(先进先出)

队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list 为起底层实现,初始化queue的语句如下:

std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。

队列是一种先进先出(First in First out,FIFO)的数据类型。每次元素的入队都只能添加到队列尾部,出队时从队列头部开始出。C++中,使用队列需要包含头文件queue。C++中队列的基本操作如下:

push() :入队。在队列尾端添加一个元素,无返回值;
pop() :出队。将队列头部元素删除(出队),无返回值;
front():获得队列头部元素。此函数返回值为队列的头部元素,常与pop()函数一起,先通过front()获得队列头部元素,然后将其从队列中删除;
size() :获得队列大小。此函数返回队列的大小,返回值是“size_t”类型的数据,“size_t”是“unsigned int”的别名。
empty():判断队列是否为空。此函数返回队列是否为空,返回值是bool类型。队列空:返回true;不空:返回false。
back():返回队列尾部元素,就是队列中最后一个进去的元素。

6.1.3

1、
C++中stack,queue 是容器么?
STL的栈和队列一般不被归类为容器,而是归类为容器适配器。
那么STL的栈用什么容器实现?
栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
栈和队列默认使用deque实现

我们使用的stack,queue是属于那个版本的STL?
我们使用的STL中stack,queue是如何实现的?
stack,queue 提供迭代器来遍历空间么?

2、问题:栈里面的元素在内存中是连续分布的么?

这个问题有两个陷阱:

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

3、
递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

6.2 用栈实现队列(4.6)

在这里插入图片描述
需要两个栈:输入栈和输出栈

在push数据的时候,只要数据放进输入栈就好,**但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),**再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。

最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。

class MyQueue {
public:
 stack<int>stin;  //输入栈
 stack<int>stout; //输出栈
 //初始化
    MyQueue() {

    }
    
    //将元素x推到队列后面
    void push(int x) {
        stin.push(x);
    }
    
    //从队列前面移除元素并返回该元素
    int pop() {
        //只有当输出栈为空的时候,再从stin里导入数据(导入stin的全部数据)
        if(stout.empty()){
            //从输入栈导入数据到输出栈
            while(!stin.empty()){
                stout.push(stin.top());    //将输入栈的顶部给输出栈
                stin.pop();
            }
        }
        int result=stout.top();
        stout.pop();
        return result;
    }
    
    //得到前面的元素
    int peek() {
        int res=this->pop();    //直接使用已有的pop函数
        stout.push(res);  //因为pop函数弹出了元素res,所以再添加回去
        return res;
    }
    
    //返回是否队列为空
    bool empty() {
        return stin.empty() && stout.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue* obj = new MyQueue();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->peek();
 * bool param_4 = obj->empty();
 */

时间复杂度: push和empty为O(1), pop和peek为O(n)
空间复杂度: O(n)

6.3 用队列实现栈(4.8)

1、还是使用两个队列,

但此时一个队列用来操作,另一个队列用来备份

具体:
把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。

class MyStack {
public:

    queue<int>que1;
    queue<int>que2;   //用于做队列的备份
    MyStack() {

    }
    
    void push(int x) {
        que1.push(x);
    }
    
    int pop() {
        int size=que1.size();    //队列的大小
        size--; //除最后一个元素外,将队列1元素导入que2
        while(size--){
            que2.push(que1.front());
            que1.pop();
        }

        int result=que1.front();      //留下的最后一个元素就是要返回的值
        que1.pop();
        que1=que2;
        while(!que2.empty()){   //清空que2,因为此时已经赋值给了que1
            que2.pop();
        }
        return result;
    }
    
    int top() {
         return que1.back();  //队列有返回尾部的操作
    }
    
    bool empty() {
        return que1.empty();
    }
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */

时间复杂度: pop为O(n),其他为O(1)
空间复杂度: O(n)

2、优化:只使用一个队列

一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。

class MyStack {
public:

    queue<int>que;
    MyStack() {

    }
    
    void push(int x) {
        que.push(x);
    }
    
    int pop() {
        int size=que.size();    //队列的大小
        size--; //除最后一个元素外,将队列头部元素重新加到队列尾部
        while(size--){
            que.push(que.front());
            que.pop();
        }

        int result=que.front();      //此时弹出的顺序就是栈的顺序
        que.pop();
        return result;
    }
    
    int top() {
         return que.back();  //队列有返回尾部的操作
    }
    
    bool empty() {
        return que.empty();
    }
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */

时间复杂度: pop为O(n),其他为O(1)
空间复杂度: O(n)

6.3 有效的括号(4.8)

栈结构的特殊性,很适合做对称匹配类的题目

首先分析不匹配情况:
三种不匹配情况
1、字符串里左方向的括号多余 → 遍历完栈仍然不为空
在这里插入图片描述

2、字符串里括号没有多余,但类型不匹配 → 遍历匹配过程中,发现没有要匹配的字符
在这里插入图片描述

3、右方向括号多余 → 遍历匹配的过程中,栈已经为空
在这里插入图片描述

class Solution {
public:
    bool isValid(string s) {
        //用栈实现
        stack<char>st;

        if(s.size()%2!=0) return false;    //如果s为奇数,不符合要求
        for(int i=0;i<s.size();i++){
            if(s[i]=='(')    st.push(')');
            else if(s[i]=='{')    st.push('}');
            else if(s[i]=='[')    st.push(']');
              // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
            // 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
            else if(st.empty() || st.top() != s[i])    return false;
            else if(st.top() == s[i])   st.pop();     //找到了匹配项就弹出
        }

  // 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
        return st.empty();
        
    }
};

时间复杂度: O(n)
空间复杂度: O(n)

6.4 删除字符串中的所有相邻重复项(4.8)

类似7.3的问题 匹配相邻元素,最后做消除

用栈来存放遍历过的元素,当遍历当前元素时,去栈里面寻找是否遍历过相同数值的相邻元素。
做消除
最后,从栈中弹出剩余元素(但弹出的元素是倒序的,所以需要对字符串反转一下)

class Solution {
public:
    string removeDuplicates(string s) {
        //使用栈来解决
        stack<char> st;
        for(int i=0;i<s.size();i++){
           if(st.empty()  ||  s[i]!=st.top()  )    st.push(s[i]);      //将s里面的字符一个个传到栈中
           else  if(st.top() == s[i])  st.pop();             //将相同元素消除
        }

   string result = "";
        while (!st.empty()) { // 将栈中元素放到result字符串汇总
            result += st.top();
            st.pop();
        }
        reverse (result.begin(), result.end()); // 此时字符串需要反转一下
        return result;


    }
};

时间复杂度: O(n)
空间复杂度: O(n)
2、字符串本身就有push_back和pop_back

class Solution {
public:
    string removeDuplicates(string S) {
        string result;
        for(char s : S) {	//对字符串S中的每个字符进行遍历操作。在每次循环中,字符s会依次代表S中的一个字符。
            if(result.empty() || result.back() != s) {
                result.push_back(s);
            }
            else {
                result.pop_back();
            }
        }
        return result;
    }
};

时间复杂度: O(n)
空间复杂度: O(1),返回值不计空间复杂

6.5 逆波兰表达式求值(4.10)

在这里插入图片描述

逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

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

逆波兰表达式相当于是二叉树中的后序遍历,可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
类似上一题

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<long long> st;   //定义一个栈
        for(int i=0;i<tokens.size();i++){
            if(tokens[i]=="+" || tokens[i]=="-"||tokens[i] == "*" || tokens[i] == "/"){
                long long num1=st.top();
                st.pop();
                long long num2 = st.top();
                st.pop();
                if (tokens[i] == "+") st.push(num2 + num1);
                if (tokens[i] == "-") st.push(num2 - num1);
                if (tokens[i] == "*") st.push(num2 * num1);
                if (tokens[i] == "/") st.push(num2 / num1);
            }
            else{
                st.push(stoll(tokens[i]));//stoll是将字符串转换为长整型数值的函数。
            }
        }
        int result=st.top();
        st.pop();
        return result;
    }
};

时间复杂度: O(n)
空间复杂度: O(n)

6.6 滑动窗口最大值(4.10)

C++中deque是stack和queue默认的底层实现容器,deque是可以两边扩展的,deque是一个双向队列。
而且deque里元素并不是严格的连续分布的

设计单调队列
1、pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
2、push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

优先级队列

自己定义一个单调队列,将有可能成为最大值的放在队口,不需要维护3之前比3小的元素(直接弹出单调队列,因为移动滑动窗口1也不会影响结果)
3之后,比3小的元素可以放进单调队列,因为滑动窗口还会向后移动

当滑动窗口pop的元素==单调队列的队口时,两者同时pop,因为在单调队列中比队口元素小的已经被弹出,队口是一段滑动窗口中元素的最大,所以不会比队口元素还大

class Solution {
private:
    //维护的单调队列
    class Myqueue{
    public:
        deque<int> que; //使用deque实现单调队列
        //分弹出、输入两部分
        //弹出时候,比较当前要弹出的数值是否等于队列前面出口元素
        void pop(int value){
            if(!que.empty() && value==que.front()){
                que.pop_front();
            }
        }
        //输入的时候,比较是否大于后面入口元素
        void push(int value){
            while(!que.empty() && value>que.back()){
                que.pop_back();
            }
            que.push_back(value);   //放入当前元素
        }
        //查询当前队列最大元素,直接返回队列前端也就是front
        int front(){
            return que.front();
        }

    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        Myqueue que;
        vector<int> result;
        int size=nums.size();
        int count=size-k+1; //滑动窗口移动次数
        //将k个元素放入队列
        for(int i=0;i<k;i++){
            que.push(nums[i]);
        }
        result.push_back(que.front());  //result记录前k个元素的最大值
        //滑动窗口移动
        for(int i=k;i<size;i++){
            que.pop(nums[i-k]); //移除滑动窗口最前面元素
            que.push(nums[i]);  //加入滑动窗口最后面元素
            result.push_back(que.front());  //记录对应的最大值
        }
        return result;
    }
};

在C++中,vector是一个动态数组容器,可以存储任意类型的元素。push_back()是std::vector类的成员函数,用于将元素添加到向量的末尾。

时间复杂度: O(n)
空间复杂度: O(k)
再来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。
其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。

空间复杂度因为我们定义一个辅助队列,所以是O(k)。

6.7 前K个高频元素(4.11)

主要思路:
1、统计元素出现的频率(用map进行统计)
2、对频率进行排序(使用优先级队列)
3、找出前K个高频元素

6.7.1 堆(大顶堆和小顶堆)

(涉及十大排序中的堆排序和优先队列)
可以想到使用map,key存放数字,value存到频次,
但map是对key进行排序的,而不是对value
除非自定义比较函数,就可以对value进行排序

大顶堆和小顶堆。(底层实现是二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。)
大顶堆:头部大,从大到小
小顶堆:头部小,从小到大

思路:用堆遍历一遍map里面的所有元素,堆里面就维持k个元素,遍历完之后堆里面的k个元素就维护了我们想要的前k个高频元素

定义:
priority_queue模板参数,优先队列有三个参数,其声明形式为:

priority_queue< type, container, function>

这三个参数,后面两个可以省略,第一个不可以。其中:
type:数据类型; container:实现优先队列的底层容器,必须是可随机访问的容器,例如vector、deque,而不能使用list;
function:元素之间的比较方式;

   在STL中,默认情况下(不加后面两个参数)是以vector为容器,以 operator< 为比较方式,所以在只使用第一个参数时,优先队列默认是一个最大堆,每次输出的堆顶元素是此时堆中的最大元素。

成员函数
假设type类型为int,则:
bool empty() const :返回值为true,说明队列为空;
int size() const :返回优先队列中元素的数量;
void pop() :删除队列顶部的元素,也即根节点
int top() :返回队列中的顶部元素,但不删除该元素;
void push(int arg):将元素arg插入到队列之中;

关于大顶堆和小顶堆的选择:
大顶堆,每次push进新元素之后,未来维护k个值,还需要pop出去一个元素,但pop的是根节点,也就是把最大的元素pop出去了。所以用大顶堆遍历一遍之后就只剩下前k个低频元素

所以要使用小顶堆,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

时间复杂度
堆每加入一个元素,进行整体调整,复杂度是logk,(因为堆就维护k个元素)
所以总复杂度是nlogk
快排需要将map转换为vector结构,然后对整个数组进行排序,复杂度nlogn

在这里插入图片描述

class Solution {
public:
// 小顶堆的比较参数(确定优先级关系),比较两个pair<int,int>的第二个参数
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
       
        // 3、找出前K个高频元素
       //1、统计元素出现的频率(用map进行统计)
        unordered_map<int,int>map;  //结构为map<nums[i],出现的次数>
        for(int i=0;i<nums.size();i++){
            map[nums[i]]++;
        }
        //2、对频率进行排序(使用优先级队列)
         // 定义一个小顶堆,大小为k
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;	//vector<pair<int, int>>是存储容器
        
        // 用固定大小为k的小顶堆,扫描所有频率的数值
        //使用迭代器it来依次访问unordered_map中的每个元素,直到遍历完整个容器为止。
        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
            pri_que.push(*it);
            if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                pri_que.pop();
            }
        }
         // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> result(k);
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pri_que.top().first;//.first优先队列中元素pair的第一个值也就是map的key
            pri_que.pop();
        }
        return result;
    }
};
  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值