代码随想录(五):栈和队列

232. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

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

说明

只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, sizeis empty 操作是合法的。

示例

MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek();  // 返回 1
queue.pop();   // 返回 1
queue.empty(); // 返回 false

解题思路

(栈,队列) O(n)
这道题目与 LeetCode 225. Implement Stack using Queues 类似。

我们用一个栈来存储队列中的元素,另外还需要一个辅助栈,用来辅助实现 pop()peek() 操作。

四种操作的实现方式如下:

push(x) – 直接将x插入栈顶;
pop() – 即需要弹出栈底元素,我们先将栈底以上的所有元素插入辅助栈中,然后弹出栈底元素,最后再将辅助栈中的元素重新压入当前栈中;
peek() – 返回栈顶元素,同理,我们先将栈底以上的所有元素插入辅助栈中,然后输出栈底元素,最后再将辅助栈中的元素重新压入当前栈中,恢复当前栈原状;
empty() – 返回当前栈是否为空;
时间复杂度分析:push(x)emtpy() 均只有一次操作,时间复杂度是 O(1)pop()peek() 涉及到 n 次操作,所以时间复杂度是 O(n)

C++代码

class MyQueue {
public:
    stack<int> sta, cache;
    MyQueue() {

    }
    
    void push(int x) {
        sta.push(x);
    }
    
    int pop() {
        while (!sta.empty()) cache.push(sta.top()), sta.pop();
        int x = cache.top();
        cache.pop();
        while (!cache.empty()) sta.push(cache.top()), cache.pop();
        return x;
    }
    
    int peek() {
        while (!sta.empty()) cache.push(sta.top()), sta.pop();
        int x = cache.top();
        while (!cache.empty()) sta.push(cache.top()), cache.pop();
        return x;
    }
    
    bool empty() {
        return sta.empty();
    }
};

225. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

注意

你只能使用队列的基本操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。

解题思路
(队列,栈) O(n)
我们用一个队列来存储栈中元素。对于栈中的四种操作:

  • push(x) – 直接入队;
  • pop() – 即需要弹出队尾元素。我们先将队首元素弹出并插入队尾,循环 n−1次,n 是队列长度。此时队尾元素已经在队首了,直接将其弹出即可;
  • top() – 即返回队尾元素。同理,我们先将队首元素弹出并插入队尾,循环 n−1 次,n 是队列长度。此时队尾元素已经在队首了,直接将其返回。不要忘记将其弹出并插入队尾,恢复队列原状;
  • empty() – 返回队列是否为空;

时间复杂度分析:push()empty() 均只有一次操作,时间复杂度是 O(1)pop()top() 需要循环 n 次,所以时间复杂度是 O(n)

C++ 代码

class MyStack {
public:
    queue<int> que;
    MyStack() {

    }
    
    void push(int x) {
        que.push(x);
    }
    
    int pop() {
        int cnt = que.size() - 1;
        while (cnt--) que.push(que.front()), que.pop();
        int x = que.front();
        que.pop();
        return x;
    }
    
    int top() {
        return que.back();
    }
    
    bool empty() {
        return que.empty();
    }
};

20. 有效的括号

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

有效字符串需满足:

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

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

提示

1 <= s.length <= 104
s 仅由括号 '()[]{}' 组成

解题思路

(栈) O(n)

从前往后枚举每个字符

  1. 当遇到左括号,则将元素压进栈中
  2. 当遇到右括号时
  • 如果栈为空,说明此时已没有左括号与其相匹配,return false
  • 如果栈顶的左括号与其右括号匹配,则将栈顶元素pop出即可;
  • 否则,说明栈顶的左括号与其不匹配,return false
  1. 遍历完所有的括号后,若栈为空,则说明所有字符都已经匹配好了;若栈不为空,则说明栈中还存在未能匹配的左括号。

注意⚠️:由于 ‘{’ 和 ‘}’ 以及 ‘(’ 和 ‘)’ 他们的字符数值只相差1,而 ‘[’ 和 ‘]’ 的字符数值只相差2,因此还可以通过这个特性简化代码,代码在最下方

C++代码
方法一

class Solution {
public:
    bool isValid(string s) {
        stack<char> stk;
        for (auto c : s) {
            if (c == '(' || c == '[' || c == '{') stk.push(c);
            else {
                if (stk.empty()) return false;
                if (stk.top() == '(' && c == ')') stk.pop();
                else if (stk.top() == '[' && c == ']') stk.pop();
                else if (stk.top() == '{' && c == '}') stk.pop();
                else return false;
            }
        }
        return stk.empty();
    }
};

方法二

class Solution {
public:
    bool isValid(string s) {
        stack<char> stk;
        for (auto c : s) {
            if (c == '(' || c == '[' || c == '{') stk.push(c);
            else {
                if (!stk.empty() && abs(stk.top() - c) <= 2) stk.pop();
                else return false;
            }
        }
        return stk.empty();
    }
};

1047. 删除字符串中的所有相邻重复项

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

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

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,
这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",
其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。

提示

  1. 1 <= S.length <= 20000
  2. S 仅由小写英文字母组成。

解题思路

可以运用栈和队列 遍历字符串中的每一个字母,当栈顶字母和当前字母不相同时,将当前元素入栈;否则,当前元素和栈顶元素相同,且栈顶字母和当前字母相邻,此时就将栈顶元素出栈。运用队列也是一个道理。

C++代码
方法一

class Solution {
public:
    string removeDuplicates(string s) {
        string res;
        for (auto c: s) {
            if (res.size() && res.back() == c) res.pop_back();
            else res += c;
        }
        return res;
    }
};

方法二

class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> res;
        for (auto c: s) {
            if (!res.empty() && res.top() == c) res.pop();
            else res.push(c);
        }
        string r;
        while (res.size()) {
            r += res.top();
            res.pop();
        }
        reverse(r.begin(), r.end());
        return r;
    }
};

150. 逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*''/'
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

示例 1:

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

示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

提示

  • 1 <= tokens.length <= 104
  • tokens[i] 是一个算符("+""-""*""/"),或是在范围 [-200, 200] 内的一个整数

逆波兰表达式

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 )
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * )

逆波兰表达式主要有以下两个优点:

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

解题思路

(栈操作) O(n)
遍历所有元素。如果当前元素是整数,则压入栈;如果是运算符,则将栈顶两个元素弹出做相应运算,再将结果入栈。
最终表达式扫描完后,栈里的数就是结果。

时间复杂度分析:每个元素仅被遍历一次,且每次遍历时仅涉及常数次操作,所以时间复杂度是 O(n)

C++ 代码

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> sta;
        for (auto &t : tokens) {
            if (t == "+" || t == "-" || t == "*" || t == "/") {
                int a = sta.top();
                sta.pop();
                int b = sta.top();
                sta.pop();
                if (t == "+") sta.push(a + b);
                else if (t == "-") sta.push(b - a);
                else if (t == "*") sta.push(a * b);
                else sta.push(b / a);
            }
            else sta.push(stoi(t));
        }
        return sta.top();
    }
};

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]

解释:
请添加图片描述

示例 2:

输入:nums = [1], k = 1
输出:[1]

提示

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length

解题思路:

滑动窗口最大值属于单调队列题型,运用for循环做以下四步:

  1. 解决队头已经弹出窗口的问题;
  2. 解决队尾与当前元素nums[i]不满足单调性的问题;
  3. 将当前元素下标加入队尾;
  4. 如果满足条件则输出结果;

注意⚠️

  1. 上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
  2. 队列中存的是原数组的下标,取值时要再套一层,nums[q[]];

C++代码

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<int> q; // q维护的是一个单调递减双端队列的下标
        vector<int> res;
        for (int i = 0; i < nums.size(); i++) {
            if (q.size() && i - k + 1 > q.front()) q.pop_front(); // 如果队头元素超出了队列所能维护的范围,将队头弹出
            while (q.size() && nums[i] >= nums[q.back()]) q.pop_back(); //如果新加入的元素比队尾所对应的元素值大,那么将队尾弹出
            q.push_back(i); //将新的元素下标插入到队尾
            if (i >= k - 1) res.push_back(nums[q.front()]); // 如果队列中的元素总数达到了k个,就将队头所对应的元素加入到结果中
        }
        return res;
    }
};

347. 前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

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

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

解题思路一

(哈希表,计数排序) O(n)
首先用 哈希表 统计出所有数出现的次数。
由于所有数出现的次数都在 1 到 n 之间,所以我们可以用 计数排序 的思想,统计出次数最多的前 k 个元素的下界。然后将所有出现次数大于等于下界的数输出。

时间复杂度分析:用哈希表统计每个数出现次数的计算量是 O(n),计数排序的计算量是 O(n),最终用下界过滤结果的计算量也是 O(n),所以总时间复杂度是 O(n)

C++ 代码

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> cnt;
        vector<int> res;
        for (int x : nums) cnt[x] ++;
        int n = nums.size();
        vector<int>s(n + 1, 0); //用s数组进行计数排序
        for (auto &p : cnt) s[p.second] ++ ; 
        int i = n, t = 0; 
        while (t < k) t += s[i -- ]; //通过t来统计前k个高频元素,分界线为i和i+1
        for (auto &p : hash)
            if (p.second > i)
                res.push_back(p.first);
        return res;
    }
};

解题思路二
首先用 哈希表 统计出所有数出现的次数。
然后将每个元素按照出现的次数进行降序排序,将出现前k个元素作为结果返回。

class Solution {
public:
    static bool comp(const pair<int,int> &a, const pair<int,int> &b) {
            return a.second > b.second;
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> cnt; //统计每个元素出现的次数
        for (auto c : nums) cnt[c]++;
        vector<pair<int,int>> b(cnt.begin(), cnt.end());
        sort(b.begin(),b.end(),comp);
        vector<int> res;
        for (int i = 0; i < k; i++) res.push_back(b[i].first);
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小浩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值