【C++初阶】Stack、Queue算法题大练兵!

◆ 博主名称:Severus-Yuan
◆ 来自泥潭,充满野心,强大冷静,从不后悔
◆ 欢迎光临我的思考天地,期待我们在这刹那的同频共振


Severus-Yuan的专栏 链接🔗
C语言 C语言
初阶数据结构与算法 初阶数据结构与算法
数据结构算法题 数据结构算法题
C++ C++

文章目录


一.Stack

(1)最小栈

最小栈

1.题解代码:
cpp 复制代码
class MinStack {
public:
    MinStack() {
         //构造函数
    }
    
    void push(int value) {
        s1.push(value);
        if(s2.empty() || value <= s2.top() )
        {
            s2.push(value);
        }
    }
    
    void pop() {
        int del = s1.top();
        s1.pop();
        if(s2.top() == del)
        {
            s2.pop();
        }
    }
    
    int top() {
        return s1.top();
    }
    
    int getMin() {
        return s2.top();
    }

    private:
      stack<int> s1;
      stack<int> s2;
};
2.图文分析:

首先先看清题目要求,使用常数时间解决问题,那么设置min变量遍历找最小值的想法就行不通,不过其实就算允许O(N)的时间复杂度,该思路也不能满足要求,当pop掉当前的最小变量之后,我们又去哪里找上一个最小变量呢?

这里我们采取两个栈的方法。一个栈s1专门用于储存所有元素,而另一个栈s2则是专门用于存储每个阶段的最小值。

push():当s1压入第一个元素时,这个元素一定是当前栈中的最小值,所以也要压入s2中(对应push中的s2.empty()),继续向s1中压入元素时,就要与s2中栈顶的元素进行比较了,只有小于等于的情况,才能压入s2中。这样才能确保每次的s2.top()是s1中的最小值;

pop():当删除s1中的元素时,如果该元素是栈的最小值,那么就要将s2中栈顶对应的最小值一起删除,这样才能保证s2中存储的最小值和s1中的真实情况同步。

MinStack():至于为啥不写MinStack()的构造函数,是因为MinStack的两个成员变量都是自定义类型stack,在MinStack()内部会自动调用stack的默认构造,库里已经实现了,不需要我们多此一举。

(2)栈的压入、弹出序列

栈的压入、弹出序列

1.题解代码:
cpp 复制代码
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     * @param pushV int整型vector 
     * @param popV int整型vector 
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        // write code here
        stack<int> s;
        size_t pushVi = 0;
        size_t popVi = 0;
        while(pushVi<pushV.size())
       {
            s.push(pushV[pushVi++]);
            //相等了
            while(!s.empty() && s.top() == popV[popVi])
            {
            //加上!s.empty(),不然等s为空时还去栈顶元素就会报错
                s.pop(); 
                popVi++;
            }
        }
        return s.empty();
    }
};
2.图文分析:

感觉这题还挺难的,我看懂解题思路之后写出代码还花了半小时。

乍一看见这道题,你可能会想到很多复杂的思路,这里就不绕弯子了,直接摆出一种比较好理解的思路。

根据题目可知,给出了入栈序列和一个可能的出栈序列,既然这样,那我们就先按着入栈序列将元素一个一个地压入栈中,但在压栈的过程中,如果栈顶的元素(栈中元素只能从栈顶出来)和出栈序列popVi下标处的元素相等(下标popVi从0开始遍历)的话,那就pop掉栈中栈顶的元素,直到栈顶元素和出栈序列popVi下标处的元素不相等了(注意:是判断栈顶元素是否和出栈序列popVi下标处的元素是否相等,不是判断入栈序列pushVi下标处的元素和出栈序列popVi下标处的元素是否相等,这会影响到内层循环的写法。该思路下,元素要先入栈才有比较资格),再继续按着入栈序列将元素压入栈中(定义了下标pushVi,也是从0开始遍历),直到将入栈序列中的所有元素压入栈中。

从这个思路我们也能知道,外层的大循环是"入栈序列中的所有元素压入栈中",而内层嵌套的小循环是"直到栈顶元素和出栈序列popVi下标处的元素不相等了";入栈和出栈序列能匹配上的结果是入栈的元素能够被全部pop掉,也就是在出大循环之后,s为空,不能匹配上,即出大循环之后,s里还有元素。

我录了个视频模拟思路过程,感兴趣地可以看看:栈的压入、弹出序列思路视频模拟。

(3)逆波兰表达式求值

逆波兰表达式求值

逆波兰表达式也就是后缀表达式,也就是将运算符写在两个操作数的后面。而我们正常写的是中缀表达式,即将运算符写在两个操作数的中间,像3+4,但如果按后缀表达式/逆波兰表达式的写法,就要写成3 4 +。

1.题解代码:
cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> s;
        int ret = 0;
        size_t i = 0;
        //遍历vector的下标
        while(i < tokens.size())
        {
            if(!(tokens[i]=="+" || tokens[i]=="-" || tokens[i]=="*" || tokens[i]=="/"))
            {
                s.push(stoi(tokens[i]));
                //是数字就入栈
            }
            else
            {
             char x = tokens[i][0];
            //右操作数
             int b = s.top();
             s.pop();
            //左操作数
             int a = s.top();
             s.pop();

             if(x == '+')
             {
                ret = a+b;
             }else if(x == '-'){
                ret = a-b;
             }else if(x == '*'){
                ret = a*b;
             }else{
                ret = a/b;
             }
             s.push(ret);
            }
            i++;
        }
        return s.top();
    }
};
cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> s;
        int ret = 0;
        size_t i = 0;
        //处理输入的vector只有一个string元素的情况
        if(tokens.size() == 1)
        {
            return stoi(tokens[0]);
        }
        
        while(i < tokens.size())
        {
            while(!(tokens[i]=="+" || tokens[i]=="-" || tokens[i]=="*" || tokens[i]=="/"))
            {
                s.push(stoi(tokens[i]));
                //是数字就入栈
                i++; 
            }

            char x = tokens[i][0];
            i++;
            //右操作数
            int b = s.top();
            s.pop();
            //左操作数
            int a = s.top();
            s.pop();

            if(x == '+')
            {
                ret = a+b;
            }else if(x == '-'){
                ret = a-b;
            }else if(x == '*'){
                ret = a*b;
            }else{
                ret = a/b;
            }
            s.push(ret);
        }
        return s.top();
    }
};
2.分析:

怎么写都行,核心思路就是遇到数字就入栈,遇到运算符就取出刚压入的两个数字作为操作数进行运算,再将所得值压入栈中,成为操作数。直到遍历完vector中的所有string,最后留在栈中的就是运算结果。

运算符的遍历顺序就是运算的优先级顺序。

(4)用栈实现队列

用栈实现队列

题解代码:
cpp 复制代码
class MyQueue {
public:
    MyQueue() {
        //构造函数
    }
    
    void push(int x) {
        pushst.push(x);
    }
    
    int pop() {
        if(popst.empty())
        {
            //popst为空就要先倒值
            while(!pushst.empty())
            {
                popst.push(pushst.top());
                pushst.pop();
            }
        }
        int n = popst.top();
        popst.pop();
        return n;
    }
    
    int peek() {
        if(popst.empty())
        {
            //popst为空就要先倒值
            while(!pushst.empty())
            {
                popst.push(pushst.top());
                pushst.pop();
            }
        }
        return popst.top();
    }
    
    bool empty() {
        return pushst.empty() && popst.empty();
    }

    private:
    stack<int> pushst;
    stack<int> popst;
};

这道题我曾在学C语言数据结构栈和队列时写过,如果不明思路的,可以去我的另一篇博客【数据结构算法题】用队列实现栈/用栈实现队列(详细解答!)看看,这里就不再赘述了。从这道题也能看出C++有了STL之后,解题就爽多了。

二.Queue

(1)用队列实现栈

用队列实现栈

题解代码:
cpp 复制代码
//1.我的实现:
//和【数据结构算法题】用队列实现栈/用栈实现队列(详细解答!)中的思路一致
class MyStack {
public:
    MyStack() {
        //构造函数
    }
    
    void push(int x) {
        if(q1.empty())
        {
            q2.push(x);
        }else{
            q1.push(x);
        }
    }
    
    int pop() {
        queue<int>* nonqempty = &q1;
        queue<int>* qempty = &q2;
        if(q1.empty())
        {
            nonqempty = &q2;
            qempty = &q1;
        }

        size_t oldsize = nonqempty->size();
        for(size_t i=0;i<oldsize-1;i++)
        {
            int tmp = nonqempty->front();
            nonqempty->pop();
            qempty->push(tmp);
        }
        int ret = nonqempty->front();
        nonqempty->pop();
        return ret;
    }
    
    int top() {
        queue<int>* nonqempty = &q1;
        queue<int>* qempty = &q2;
        if(q1.empty())
        {
            nonqempty = &q2;
            qempty = &q1;
        }
        
        size_t oldsize = nonqempty->size();
        for(size_t i=0;i<oldsize-1;i++)
        {
            int tmp = nonqempty->front();
            nonqempty->pop();
            qempty->push(tmp);
        }
        int ret = nonqempty->front();
        nonqempty->pop();
        qempty->push(ret);
        return ret;
    }
    
    bool empty() {
         return q1.empty() && q2.empty();
    }

private:
    queue<int> q1;
    queue<int> q2;
};

这道题我曾在学C语言数据结构栈和队列时写过,如果不明思路的,可以去我的另一篇博客【数据结构算法题】用队列实现栈/用栈实现队列(详细解答!)看看,这里就不再赘述了。

cpp 复制代码
//官方提供的一种解法:
class MyStack {
public:

    MyStack() {
        //构造函数
    }
    
    void push(int x) {
        //每次都从queue2入队,则每次入队的那一个元素都是模拟栈的栈顶元素
        queue2.push(x);
        while(!queue1.empty())
        {
            //queue1中现有的元素也是由每次的栈顶元素累积得来的
            queue2.push(queue1.front());
            queue1.pop();
        }
        swap(queue1,queue2);
    }
    
    int pop() {
        int ret = queue1.front();
        queue1.pop();
        return ret;
    }
    
    int top() {
        return queue1.front();
    }
    
    bool empty() {
        return queue1.empty();
    }
    
    private:
    queue<int> queue1;
    queue<int> queue2;
};

不得不感叹,官方确实牛逼,不仅实现的代码简洁,就连解释也写得精炼而深刻。

(2)二叉树的层序遍历

二叉树的层序遍历

1.题解代码:
cpp 复制代码
//我的实现:
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ret;
        if(root == nullptr)
          return ret;
        //非空树

        queue<TreeNode*> q;
        //存节点的队列
        q.push(root);
        while(!q.empty())
        {
            vector<int> v;
            size_t leversize = q.size();
            while(leversize--)
          {
            //完成root的迭代
            auto node = q.front();
            v.push_back(node->val);
            q.pop();
            if(node->left)
             q.push(node->left);
            if(node->right)
             q.push(node->right);
          }
          ret.push_back(v);
        }

        return ret;
    }
};
cpp 复制代码
//官方提供的解法:
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ret;
        if (!root) {
            return ret;
        }

        //队列装节点而不是节点的值
        queue <TreeNode*> q;
        q.push(root);
        while (!q.empty()) 
        {
            int currentLevelSize = q.size();
            ret.push_back(vector<int>());
            //匿名对象
            //放vector<int>的目的是题目要求返回二维数组
            for (int i = 1; i <= currentLevelSize; ++i) 
            {
                //最后的两个push加上front间接完成了node的更新
                TreeNode* node = q.front(); 
                q.pop();
                ret.back().push_back(node->val);
                if (node->left) 
                  q.push(node->left);
                if (node->right) 
                  q.push(node->right);
            }
        }  
        return ret;
    }
};

我的实现和官方的实现的思路其实大差不差,最大的区别就是我使用的是vector< int >的有名对象,但是官方使用的是vector< int >的匿名对象。

2.分析:

这题感觉有点难度,一开始的思路有错误后面就容易陷入复杂的局面。

在学习二叉树时,实现二叉树的某个层序遍历的题目时貌似就是借助队列完成的。先将根节点入队,当pop根节点的同时将左右子节点入队,再当pop根节点的左孩子节点的同时,将根节点的左孩子节点的左右子节点入队,根节点的右孩子节点也是同样的逻辑,之后的节点都是这样的逻辑。这样,二叉树中的所有节点能按层序遍历的顺序出现在队列中,具体的题目我忘记了,但这个思路是很重要的。

在这个思路的前提下,我们来看看代码究竟是怎么实现的:

首先先定义一个队列q,用于存树的节点。树的每个节点会按照层序遍历的顺序"流过"该队列,当队列中存储的元素都来自树的同一层时,我们要将此时的所有元素放进v里,再插入到ret里。但是由于队列中既可能出现存储元素都是树的同一层的情况,也可能出现不同层的情况,所以我们还需要定义一个leversize变量,用于记录每层节点的个数(树的每层的节点个数也就是队列的大小)。根据levelsize的大小进行pop且同时插入子节点,就能确保队列中存储的元素都来自于树的同一层。然后再将队列中来自同一层的元素放进v里,每次levelsize消耗完后,就将v插入ret中,成为二维数组的一个元素(听起来像是一次性将队列中来自树的同一层的节点插入v中,但其实是在队列pop元素的同时进行节点数据的插入,因为pop进行levelsize次的这levelsize个节点来自树的同一层)。

------end------

相关推荐
唐青枫2 小时前
C 语言 strcpy() 用法详解与实战 Demo
c语言
2601_967979123 小时前
机器学习必修课:经典算法与Python实战
python·算法·机器学习·实战·课程
程序猿打工仔3 小时前
计算机基础(C语言)考编、专技岗
c语言·开发语言·算法
通信小呆呆3 小时前
电子战干扰技术:从“蛮力压制”到“精准欺骗”,再到“认知博弈”
算法·通信·isac
老师我太想进步了20264 小时前
【数据结构-6】算法美学之单链表(1)
数据结构·算法
xiaoye-duck4 小时前
《C++ 项目:手写大模型接入 SDK》DataManager数据管理模块:SQLite持久化存储
开发语言·数据库·c++·sqlite
希望我万事顺遂5 小时前
力扣hot100-滑动窗口最大值
数据结构·算法·leetcode
꧁细听勿语情꧂5 小时前
二叉树余题,排序、直接插入排序、希尔排序
c语言·开发语言·数据结构·算法
是main不是漫5 小时前
【双指针|LeetCode1089 复写零】巧用逆向思路,破解原地覆盖陷阱
数据结构·算法·leetcode