【LeetCode Cookbook(C++ 描述)】一刷栈、队列

本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典栈和队列算法题,以后会更新“二刷”“三刷”等等。

LeetCode #20:Valid Parentheses 有效的括号

#20
给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

括号匹配是使用栈解决的经典问题。我们可以维护一个栈,遍历字符串,遇到左括号就入栈,遇到右括号则取出栈顶元素,判断栈顶元素与右括号是否匹配

其中,有两种特殊情况也会返回 false

  • 遍历完字符串,栈不为空,说明左括号多了。
  • 遇到右括号,但此时栈为空,说明右括号多了。
class Solution {
public:
    bool isValid(string s) {
        //字符串有奇数个元素,必然不匹配
        if (s.size() % 2 == 1) return false;
        //初始化栈
        stack<char> stk;
        //括号对映射
        unordered_map<char, char> pairs = {
            {')', '('},
            {']', '['},
            {'}', '{'}
        };

        for (char c : s) {
            //如果是左括号,压入栈
            if (pairs.find(c) == pairs.end()) stk.push(c);
            //在遍历过程中,碰到空栈或者括号不匹配,返回 false
            else if (stk.empty() || pairs[c] != stk.top()) return false;
            //如果是右括号,并且与栈顶左括号匹配,弹出栈顶元素
            else stk.pop();
        }
        //全部遍历完,如果栈为空,返回 true,栈不为空,返回 false
        return stk.empty();
    }
};

LeetCode #150:Evaluate Reverse Polish Notation 逆波兰表达式求值

#150
根据逆波兰表示法,求表达式的值。

有效算符包括 + - * / 。运算对象可以是整数,可以是其他逆波兰表达式。

逆波兰表达式发明出来就是为了方便计算机运用「栈」进行表达式运算(计算机普遍采用栈式结构,执行的是先入后出的操作)的,其运算规则如下:

  • 按顺序遍历逆波兰表达式中的字符,如果是数字,则放入栈。
  • 如果是运算符,则将栈顶的两个元素(先出栈的数在运算符右侧,后出栈的在运算符左侧)拿出来进行运算,再将结果放入栈。
  • 对于减法和除法,栈顶第二个数是被除(减)数。

试举一例,a + b 的逆波兰表达式为 a b + 。而逆波兰表达式有以下优点:

  • 去掉括号后表达式无歧义,(1 + 2) * (3 + 4) 即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算。
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        //初始化栈
        stack<int> stk;
        unordered_map<string, function<int(int, int)>> ops = {
            {"+", [](int a, int b) { return a + b; }},
            {"-", [](int a, int b) { return a - b; }},
            {"*", [](int a, int b) { return a * b; }},
            {"/", [](int a, int b) { return b != 0 ? a / b : throw runtime_error("Division by zero"); }}
        };

        for (const string& token : tokens) {
            //如果当前 token 不是操作符,转换为整数并入栈
            if (token != "+" && token != "-" && token != "*" && token != "/") stk.push(stoi(token));
            else {  //如果当前token是操作符
                //先出栈的数在操作符右侧
                int right = stk.top(); stk.pop();
                //后出栈的数在操作符左侧
                int left = stk.top(); stk.pop();
                //进行相应运算,运算完的数值入栈
                stk.push(ops[token](left, right));  //经典的条件判断语句效率更高,可采用
            }
        }
        return stk.top();
    }
};

LeetCode #232:Implement Queue using Stacks 用栈实现队列

#232
仅使用两个栈实现先入先出队列,队列支持一般队列支持的所有操作:

  • void push(int x) :将元素 x 推到队列的末尾。
  • int pop() :从队列的开头移除并返回元素。
  • int peek() :返回队列开头的元素。
  • boolean empty() :如果队列为空,返回 true ;否则,返回 false

队列是一种先入先出(FIFO)的数据结构,而栈是一种后入先出(LIFO)的数据结构,一个栈绝对满足不了队列的 FIFO 特性。因此,我们需要两个栈——输入栈 s1输出栈 s2 ,输入栈用以反转元素的入队顺序,元素只能从输入栈进入( push );输出栈用以存储元素的正常顺序,元素只能从输出栈出来(poppeek )。

当调用 push() 让元素入队时,只要把元素压入栈 s1 即可;当调用 pop() 让元素出队时,若输出栈 s2 不为空,直接弹出栈顶元素,若 s2 为空,则把输入栈 s1 的所有元素压入输出栈 s2 中,再弹出 s2 栈顶元素。

其余的函数调用较为简单,empty() 判断队列是否为空,如果两个栈都为空则队列为空,反之亦然;peek() 方法则与 pop() 思路一致,只要查看 s2 栈顶元素即可。

class MyQueue {
private:
    stack<int> s1, s2;

public:
    MyQueue() {
        
    }
    
    void push(int x) {
        s1.push(x);
    }
    
    int pop() {
        //保证 s2 非空
        peek();
        int poppedValue = s2.top();
        s2.pop();

        return poppedValue;
    }
    
    int peek() {
        if (s2.empty())
            while (!s1.empty()) {
                //把 s1 元素压入 s2
                s2.push(s1.top());
                s1.pop();
            }
        return s2.top();
    }
    
    bool empty() {
        return s1.empty() && s2.empty();
    }
};

LeetCode #225:Implement Stack using Queues 用队列实现栈

#225
用队列实现栈的下列操作:

  • void push(int x) :元素 x 入栈。
  • int pop() :移除并返回栈顶元素。
  • int peek() :返回栈顶元素。
  • boolean empty() :栈为空,返回 true ,否则返回 false

建一个主队列和一个辅助队列,每次入栈操作时,将新元素添加到辅助队列,再依次将主队列的元素出队列,依次加入辅助队列,最后将主队列与辅助队列互换。

class MyStack {
private:
    queue<int> q1; //主队列,模拟栈的行为
    queue<int> q2; //辅助队列

public:
    MyStack() {
    
    }
    
    void push(int x) {
        q2.push(x);    //先将新元素推入辅助队列
        while (!q1.empty()) {
            q2.push(q1.front());   //将主队列的所有元素移到辅助队列
            q1.pop();
        }
        //交换两个队列
        queue<int> temp = q1;
        q1 = q2;
        q2 = temp;
    }
    
    int pop() {
        int topElement = q1.front();    //获取栈顶元素
        q1.pop();     //移除栈顶元素
        return topElement;
    }
    
    int top() {
        return q1.front();    //返回栈顶元素,但不移除它
    }
    
    bool empty() {
        return q1.empty();    //如果主队列为空,则栈为空
    }
};

LeetCode #239 :Sliding Window Maximum 滑动窗口最大值

#239
大小为 k 的滑动窗口从整数数组 nums 的最左侧移到最右侧,只能看到滑动窗口中的 k 个数字,窗口每次向右移动一位。

返回滑动窗口的最大值。

这是用队列解决的经典滑动窗口问题。一般的队列是限制仅在队尾插入、在队首进行删除操作的线性表,而双端队列则是放开了这个限制,在两端都可以进行入队和出队操作,类似于栈和队列的结合体。存在两种受限的双端队列,一种是输出受限的双端队列,允许在一端进行入队和出队,但在另一端只允许入队;另一种则是输入受限的双端队列,允许在一端进行入队和出队,但在另一端只允许出队。

进一步地,输出受限的双端队列中有一种特殊的形式——队列里的各元素之间的关系具有单调性,即单调队列,也是解决本问题的关键。单调队列这一数据结构,既能够维护队列元素先进先出的时间顺序,又能够正确维护队列中所有元素的最值。

我们维护一个单调递减的单调队列(队列只存储数组对应的索引值),较大的元素必然比较小的元素晚弹出队列,从而只在队列中保留可能成为窗口最大元素的元素,去掉不可能成为窗口中最大元素的元素。

首先,初始化一个单调队列和结果数组,并注意参数 k 不合法的情况:

if (nums.empty() || k <= 0 || k > nums.size()) return {};

deque<int> dq; //双端队列,保存当前窗口最大值的数组位置
vector<int> res(nums.size() - k + 1, 0); //结果数组,初始化大小为nums.size() - k + 1

对于每一个新进入的元素,只要大于等于先前的队尾元素,队尾元素就直接弹出,不予考虑,直到把所有小于等于新元素的队列元素全部弹出;同时新元素加入队列:

//保证队列从大到小,如果队尾元素小于等于当前元素,则弹出队尾
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();

dq.push_back(i);   //添加当前值的索引到队列

维护队列的单调性,确保队首是当前窗口中的最大值,并弹出超出当前窗口索引的队首元素:

if (dq.front() <= i - k) dq.pop_front();   // i - k + 1 是当前窗口的起始索引

窗口的滑动是通过循环的迭代和队列的更新来隐式地实现的,i 的每次迭代都会使得窗口右移一格。遍历完整个窗口后记录最大值到结果数组 res 中,在下次迭代中滑动窗口:

if (i >= k - 1) res[i - k + 1] = nums[dq.front()];

最终的代码实现如下:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if (nums.empty() || k <= 0 || k > nums.size()) return {};

        deque<int> dq; //双端队列,保存当前窗口最大值的数组位置
        vector<int> res(nums.size() - k + 1, 0); //结果数组,初始化大小为nums.size() - k + 1

        for (int i = 0; i < nums.size(); ++i) {
            //保证队列从大到小,如果队尾元素小于等于当前元素,则弹出队尾
            while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();

            dq.push_back(i);   //添加当前值的索引到队列
			//如果队列首部的索引已经超出窗口范围,则弹出
            if (dq.front() <= i - k) dq.pop_front();
			//当窗口大小达到 k 时,记录当前窗口的最大值
            if (i >= k - 1) res[i - k + 1] = nums[dq.front()];
        }

        return res;
    }
};

LeetCode #1047:Remove All Adjacent Duplicates in String 删除字符串中的所有相邻重复项

#1047
小写字母组成字符串 s ,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

s 上反复执行重复项删除操作,直到无法继续删除。

维护一个栈,从左到右依次扫描字符串,比较当前字符与栈顶元素:

  • 若相同,则栈顶元素出栈,当前字符不入栈。
  • 若不同,则当前字符入栈。

为了方便处理字符串,我们直接使用 C++ 中的 string 类来模拟栈,push_back() 方法添加字符到字符串的末尾(相当于入栈), pop_back() 方法移除字符串末尾的字符(相当于出栈),empty() 方法检查字符串(栈)是否为空,以及 back() 方法访问字符串(栈)的最后一个元素(栈顶元素)。

class Solution {
public:
    string removeDuplicates(string s) {
        string res; //使用 string 作为栈

        for (char c : s) {
            //当栈不为空且栈顶元素等于当前字符时,弹出栈顶元素
            if (!res.empty() && res.back() == c) res.pop_back();
            else res.push_back(c);  //否则,将当前字符入栈 
        }
        return res;
    }
};

呜啊?

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值