大家好,我是此林。
今天分享的是【代码随想录】栈与队列算法专题总结,分享刷算法的心得体会。
1. 用栈实现队列、用队列实现栈
232. 用栈实现队列 - 力扣(LeetCode)
225. 用队列实现栈 - 力扣(LeetCode)
这两题放在一起,比较相似。
1. 用栈实现队列:使用两个栈,根本是让出栈顺序和队列一样
stack1 = new Stack<>(); // 主栈,和队列顺序一致
stack2 = new Stack<>(); // 辅助栈
2. 用队列实现栈:使用两个队列,根本是让出队顺序一样
queue1 = new LinkedList<>(); // 主队列,和栈顺序一致
queue2 = new LinkedList<>(); // 辅助队列
所以无论是队列还是栈,只要重点改push的逻辑即可。
class MyQueue {Stack<Integer> stack1;Stack<Integer> stack2;public MyQueue() {stack1 = new Stack<>();stack2 = new Stack<>();}public void push(int x) {// 重点改这里的逻辑}public int pop() {return stack1.pop();}public int peek() {return stack1.peek();}public boolean empty() {return stack1.isEmpty();}
}
class MyStack {Queue<Integer> queue1;Queue<Integer> queue2;public MyStack() {queue1 = new LinkedList<>();queue2 = new LinkedList<>();}public void push(int x) {// 重点改这里的逻辑}public int pop() {return queue1.poll();}public int top() {return queue1.peek();}public boolean empty() {return queue1.isEmpty();}
}
其他方法,因为 stack1 或者 queue1 和 队列 或者 栈 的顺序一致,所以只需要操作对应方法即可。
用栈实现队列完整代码:
class MyQueue {Stack<Integer> stack1;Stack<Integer> stack2;public MyQueue() {stack1 = new Stack<>();stack2 = new Stack<>();}public void push(int x) {while (!stack1.isEmpty()) {stack2.push(stack1.pop());}stack2.push(x);while (!stack2.isEmpty()) {stack1.push(stack2.pop());}}public int pop() {return stack1.pop();}public int peek() {return stack1.peek();}public boolean empty() {return stack1.isEmpty();}
}
用队列实现栈完整代码:
class MyStack {Queue<Integer> queue1;Queue<Integer> queue2;public MyStack() {queue1 = new LinkedList<>();queue2 = new LinkedList<>();}public void push(int x) {queue2.offer(x);while (!queue1.isEmpty()) {queue2.offer(queue1.poll());}Queue<Integer> tmp = queue1;queue1 = queue2;queue2 = tmp;}public int pop() {return queue1.poll();}public int top() {return queue1.peek();}public boolean empty() {return queue1.isEmpty();}
}
2. 有效的括号
20. 有效的括号 - 力扣(LeetCode)
这题本质思路:实现一个栈,
1. 如果是左括号直接入栈,
2. 如果是右括号则和第一个出栈的左括号比对,如果匹配,左括号出栈。
如此循环,最后看栈是否Empty,如果不为空,则非有效括号(false)。
那么左右括号比对,可以写一个HashMap做映射。
class Solution {public boolean isValid(String s) {Map<Character, Character> map = new HashMap<>();map.put(']', '[');map.put(')', '(');map.put('}', '{');Stack<Character> stack = new Stack<>();for (char c : s.toCharArray()) {// 如果 c 是左括号,直接入栈if (c == '(' || c == '[' || c == '{') {stack.push(c);} else {if (!stack.isEmpty() && stack.peek() == map.get(c)) {stack.pop();} else {stack.push(c);}}}return stack.isEmpty();}
}
3. 删除字符串中的所有相邻重复项
1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
这题和 有效括号 的思路是一样的,甚至更加简单。
有效括号 还需要定义HashMap来做左右括号的匹配映射,本题只需要判断是否相等即可。
完整代码:
class Solution {public String removeDuplicates(String s) {Stack<Character> stack = new Stack<>();for (char c : s.toCharArray()) {if (!stack.isEmpty() && stack.peek() == c) {stack.pop();} else {stack.push(c);}}String res = "";while (!stack.isEmpty()) {res = stack.pop() + res;}return res;}
}
需要注意的是,最后出栈顺序会颠倒,所以不能直接 res += stack.pop(),
要改成 res = stack.pop() + res。
4. 逆波兰表达式求值
150. 逆波兰表达式求值 - 力扣(LeetCode)
这题其实也不难,
整体思路是遍历 tokens,如果是数字,直接入栈;
如果是运算符(+、-、*、/),那么就 stack.pop() 出栈两次,然后把取得两个数字做对应的运算,得到的结果入栈即可。
完整代码:
class Solution {public int evalRPN(String[] tokens) {Stack<Integer> stack = new Stack<>();for (String s : tokens) {switch (s) {case "+":stack.push(Integer.valueOf(stack.pop()) + Integer.valueOf(stack.pop()));break;case "-":int a = Integer.valueOf(stack.pop());int b = Integer.valueOf(stack.pop());stack.push(b - a);break;case "*":stack.push(Integer.valueOf(stack.pop()) * Integer.valueOf(stack.pop()));break;case "/":a = Integer.valueOf(stack.pop());b = Integer.valueOf(stack.pop());stack.push(b / a);break;default:// 数字直接入栈stack.push(Integer.valueOf(s));}}return stack.pop();}
}
可以看到,一个 switch 解决一切。
需要注意的是,减法和出除法需要颠倒下位置。
虽然这样写思路可能比较清晰,但是代码有点冗余,特别是 Integer.valueOf() 被频繁使用。
改写优化:
class Solution {public int evalRPN(String[] tokens) {Set<String> set = new HashSet<>();set.add("+");set.add("-");set.add("*");set.add("/");Stack<Integer> stack = new Stack<>();for (String token : tokens) {// 如果 token 为数字if (!set.contains(token)) {stack.push(Integer.valueOf(token));} else {Integer a = stack.pop();Integer b = stack.pop();if (token.equals("+")) {stack.push(a + b);} else if (token.equals("-")) {stack.push(b - a);} else if (token.equals("*")) {stack.push(a * b);} else if (token.equals("/")) {stack.push(b / a);}}}return stack.pop();}
}
5. 前 K 个高频元素
347. 前 K 个高频元素 - 力扣(LeetCode)
这题没什么花头,常规思路而已:
1. 遍历nums,然后用HashMap统计每个num的出现次数
2. 根据出现次数对HashMap进行排序
重点在这个排序,因为题目说时间复杂度 必须 优于 O(n log n)
所以排序用Java里现成的优先队列 PriorityQueue 即可,设置为大顶堆即可。
完整代码:
class Solution {public int[] topKFrequent(int[] nums, int k) {// 统计每个num出现的次数Map<Integer, Integer> map = new HashMap<>();for (int num : nums) {map.put(num, map.getOrDefault(num, 0) + 1);}// 对map进行从大到小排序PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o2[1] - o1[1]);for (Map.Entry<Integer, Integer> entry : map.entrySet()) {int[] tmp = new int[2];tmp[0] = entry.getKey();tmp[1] = entry.getValue();queue.offer(tmp);}// 封装TopK结果int[] res = new int[k];for (int i = 0; i < k; i++) {res[i] = queue.poll()[0];}return res;}
}
这里第一部分统计出现的次数没什么好说,
第二部分,PriorityQueue 是 Java 封装好的排序队列,每次加入队列的元素会自动排序,排序规则在构造方法里传入了:(o1, o2) -> o2[1] - o1[1]
这个tmp数组,tmp[0] 代表num,tmp[1] 代表num出现的次数
(o1, o2) -> o2[1] - o1[1] 其实就表示按照num出现的次数从大到小逆序排序。
6. 最小栈
155. 最小栈 - 力扣(LeetCode)
看到题目,第一反应就是新建一个 Stack<Integer> 和 一个 Integer min 变量。
于是,你写了下面的代码:
class MinStack {Stack<Integer> stack;int min = Integer.MAX_VALUE;public MinStack() {stack = new Stack<>();}public void push(int val) {if (val < min) {min = val;}stack.push(val);}public void pop() {stack.pop();}public int top() {return stack.peek();}public int getMin() {return min;}
}
这就有个问题,看下面例子:
入栈 3
| | min = 3
| |
|_3_|
stack 入栈 5
| | min = 3
| 5 |
|_3_|
stack 入栈 2
| 2 | min = 2
| 5 |
|_3_|
stack 出栈 2
| | min = 2?
| 5 |
|_3_|
stack
如果只用一个变量就会遇到一个问题,如果把 min
更新为 2
,那么之前的最小值 3
就丢失了。
怎么把 3
保存起来呢?把它在 2
之前压入栈中即可。
入栈 2 ,同时将之前的 min 值 3 入栈,再把 2 入栈,同时更新 min = 2
| 2 | min = 2
| 3 |
| 5 |
|_3_|
stack 入栈 6
| 6 | min = 2
| 2 |
| 3 |
| 5 |
|_3_|
stack 出栈 6
| 2 | min = 2
| 3 |
| 5 |
|_3_|
stack 出栈 2 ,然后再出栈一次,把 3 赋值给 min 即可。
| | min = 3
| |
| 5 |
|_3_|
stack
完整代码:
class MinStack {Stack<Integer> stack;int min = Integer.MAX_VALUE;public MinStack() {stack = new Stack<>();}public void push(int val) {// 如果min需要变更了,先把旧的min入栈保存,再入栈新的min,同时更新minif (val <= min) {stack.push(min);min = val;}stack.push(val);}public void pop() {// 如果发现出栈的元素等于min,那么再出栈一次,并把min重新赋值if (stack.pop() == min) {min = stack.pop();}}public int top() {return stack.peek();}public int getMin() {return min;}
}
所以,这里的核心,就是把旧的min压栈做副本保存,再入栈新的min,同时更新min。
后续 pop() 出栈,如果发现出栈的元素等于min,就出栈两次即可。
当然,还有一种官方的解法,他另外用了一个辅助栈,其实思路大差不差,都是为了缓存旧的min。这里可以看下他的思路。
class MinStack {Stack<Integer> stack;Stack<Integer> stack1; // 辅助栈public MinStack() {stack = new Stack<>();stack1 = new Stack<>();stack1.push(Integer.MAX_VALUE);}public void push(int val) {stack.push(val);stack1.push(Math.min(val, stack1.peek()));}public void pop() {stack.pop();stack1.pop();}public int top() {return stack.peek();}public int getMin() {return stack1.peek();}
}
理解算法,重点在于带入例子,去理解他的过程,没有捷径可以走。
7. 字符串解码
394. 字符串解码 - 力扣(LeetCode)
这种题目就是纯模拟题,没有什么技巧可言。
总体思路:
1. 遍历字符串,如果不是 ],即数字或 [,直接入栈。
2. 如果是 ]
3. stack.pop() + while循环 提取出需要重复的字符串 tmp
4. stack.pop() + while循环 提取出需要重复的次数 time
5. 根据 time 和 tmp 重复字符串入栈,最后整合结果。
完整代码:
class Solution {public String decodeString(String s) {Stack<Character> stack = new Stack<>(); // 遍历字符串for (char c : s.toCharArray()) {// 如果不是 ] ,是数字或 [ ,直接入栈if (c != ']') {stack.push(i);} else {// 提取出需要遍历的字符串 tmpString tmp = "";while (stack.peek() != '[') {tmp = stack.pop() + tmp;}// 删掉 [stack.pop();// 提取出需要重复的次数 timeString time = "";while (!stack.isEmpty() && stack.peek() >= '0' && stack.peek() <= '9') {time = stack.pop() + time;}// 根据 tmp 和 time 重复出字符串入栈for (int i = 0; i < Integer.valueOf(time); i++) {for (char t : tmp.toCharArray()) {stack.push(t);}}}}// 整合结果为字符串String res = "";while (!stack.isEmpty()) {res = stack.pop() + res;}return res;}
}
8. 每日温度
从 每日温度 开始,往后题目均为 单调栈 的解题思路。
单调栈 模板:
Stack<Integer> stack = new Stack<>();for (int i = 0; i < arr.length; i++) {while (!stack.isEmpty() && arr[i] > arr[stack.peek()]) {// ...}stack.push(i);}
739. 每日温度 - 力扣(LeetCode)
这题其实暴力法可以做,
第一个for循环遍历 temperatures,第二个for循环负责去找第一个比当前元素大的位置。
但是,本题用 单调栈 即可。
先明确两个事情:
1. 这个栈里存的元素是 数组下标,而不是实际元素。因为后续要计算间隔天数。
2. 这个栈是单调的,不过不是数组下标单调,而是数组下标对应的 temperature 每次入栈都应该比栈顶的要大。
为了方便理解,我们来理解过程。
首先,明确下面三个变量。
temperatures = [73,74,75,71,69,72,76,73] // 题目给出的温度列表Stack<Integer> stack = new Stack<>() // 单调栈int[] res = [0,0,0,0,0,0,0,0] // 最终答案
1. 当 i=0 时,单调栈为空,因此将 0 进栈。
stack=[0(73)]
ans=[0,0,0,0,0,0,0,0]
2. 当 i=1 时,由于 74 大于 73,因此移除栈顶元素 0,赋值 ans[0]:=1−0,将 1 进栈。
stack=[1(74)]
ans=[1,0,0,0,0,0,0,0]
注:ans[0]:=1−0 表示计算天数间隔。
3. 当 i=2 时,由于 75 大于 74,因此移除栈顶元素 1,赋值 ans[1]:=2−1,将 2 进栈。
stack=[2(75)]
ans=[1,1,0,0,0,0,0,0]
4. 当 i=3 时,由于 71 小于 75,因此将 3 进栈。
stack=[2(75),3(71)]
ans=[1,1,0,0,0,0,0,0]
5. 当 i=4 时,由于 69 小于 71,因此将 4 进栈。
stack=[2(75),3(71),4(69)]
ans=[1,1,0,0,0,0,0,0]
6. 当 i=5 时,由于 72 大于 69 和 71,因此依次移除栈顶元素 4 和 3,赋值 ans[4]:=5−4 和 ans[3]:=5−3,将 5 进栈。
stack=[2(75),5(72)]
ans=[1,1,0,2,1,0,0,0]
7. 当 i=6 时,由于 76 大于 72 和 75,因此依次移除栈顶元素 5 和 2,赋值 ans[5]:=6−5 和 ans[2]:=6−2,将 6 进栈。
stack=[6(76)]
ans=[1,1,4,2,1,1,0,0]
8. 当 i=7 时,由于 73 小于 76,因此将 7 进栈。
stack=[6(76),7(73)]
ans=[1,1,4,2,1,1,0,0]
完整代码:
class Solution {public int[] dailyTemperatures(int[] temperatures) {Stack<Integer> stack = new Stack<>();int[] res = new int[temperatures.length];for (int i = 0; i < temperatures.length; i++) {while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {int idx = stack.pop();res[idx] = i - idx;}stack.push(i);}return res;}
}
9. 柱状图中的最大的矩形
84. 柱状图中最大的矩形 - 力扣(LeetCode)
这题也是单调栈的思路。这里进入单调栈的还是数组索引下标,和 每日温度 一样
先说下算法的硬流程。
1. 输入 heights = [2, 1, 5, 6, 2, 3]
2. 加入哨兵(其实就是首尾两个0,方便后续代码):
heights = [0, 2, 1, 5, 6, 2, 3, 0]
3. 进入遍历 heights 逻辑
完整代码:
class Solution {public int largestRectangleArea(int[] heights) {// 加入哨兵的逻辑int[] newHeights = new int[heights.length + 2];newHeights[0] = newHeights[newHeights.length - 1] = 0;for (int i = 0; i < heights.length; i++) {newHeights[i + 1] = heights[i];}int res = 0;Stack<Integer> stack = new Stack<>();// 遍历 heights 逻辑for (int i = 0; i < newHeights.length; i++) {while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {int idx = stack.pop();int w = i - stack.peek() - 1; // right - left - 1int h = newHeights[idx];res = Math.max(w * h, res);}stack.push(i);}return res;}
}
遍历 heights,是要以当前高度为基准,寻找最大的宽度 组成最大的矩形面积那就是要找左边第一个小于当前高度的下标left,再找右边第一个小于当前高度的下标right,那宽度就是这两个下标之间的距离了,但是要排除这两个下标,所以是 right - left - 1,用单调栈就可以很方便确定这两个边界了。
10. 下一个更大元素 I
496. 下一个更大元素 I - 力扣(LeetCode)
这题本来想用暴力做法先熟悉下题目的,结果暴力做法直接通过了......
完整代码也贴一下,这里就不多说了。
class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {int[] res = new int[nums1.length];for (int k = 0; k < nums1.length; k++) {OUT:for (int i = 0; i < nums2.length; i++) {if (nums1[k] == nums2[i]) {for (int j = i; j < nums2.length; j++) {if (nums2[j] > nums1[k]) {res[k] = nums2[j];break OUT;}}res[k] = -1;}}}return res;}
}
这题依旧可以用单调栈法。
完整代码:
class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {// 构建HashMap,映射为 nums[i] -> iMap<Integer, Integer> map = new HashMap<>();for (int i = 0; i < nums1.length; i++) {map.put(nums1[i], i);}Deque<Integer> stack = new LinkedList<>();int[] res = new int[nums1.length];Arrays.fill(res, -1); // -1 表示不存在// 遍历 nums2for (int x : nums2) {while (!stack.isEmpty() && x > stack.peek()) {// x 是栈顶的下一个更大元素// 既然栈顶已经算出答案,弹出res[map.get(stack.pop())] = x; // 记录结果到res}// 只有 nums1 存在的元素才入栈if (map.containsKey(x)) {stack.push(x);}}return res;}
}
举个例子过程:
nums1 = [4,1,2], nums2 = [1,3,4,2], stack = new Stack<>(), res = [-1, -1, -1]
1. 遍历 nums2
2. 第一个元素 1 在nums1里,此时 stack 为 empty,直接加入 stack。
stack:栈底 → [1]
3. 第二个元素 3 不在nums1里,元素 3 大于 stack.peek(),记录结果 3 到 res 数组。
同时 stack.pop(),
stack:栈底,res = [-1, 3, -1]
4. 第三个元素 4 在nums1里,此时 stack 为 empty,直接加入 stack。
stack:栈底 → [4]
5. 第四个元素 2 在nums1里,元素 2 小于 stack.peek(),不记录结果。
stack:栈底 → [4] → [2]
11. 下一个更大元素 II
503. 下一个更大元素 II - 力扣(LeetCode)
和 下一个更大元素 I 不同的是,这里的 nums 变成了 循环数组,也就是下一个更大的元素可能不在后面,但可能在前面!
这里用两次遍历即可解决,在 下一个更大元素 I 的基础上改下即可。
完整代码:
class Solution {public int[] nextGreaterElements(int[] nums) {int n = nums.length;// 本题入栈的是数组下标Deque<Integer> stack = new LinkedList<>();int[] res = new int[n];Arrays.fill(res, -1);// 这里改为 n*2for (int i = 0; i < 2 * n; i++) {int x = nums[i % n];while (!stack.isEmpty() && x > nums[stack.peek()]) {res[stack.pop()] = x;}// 只有下标小于n的才入栈if (i < n) {stack.push(i);}}return res;}
}
12. 接雨水
42. 接雨水 - 力扣(LeetCode)
用了单调栈,从此接雨水可以秒了。(真的吗?)
不管怎么说,先看看暴力解法吧,也可以通过,这个是理解题目的基础
class Solution {public int trap(int[] height) {int sum = 0;for (int i = 0; i < height.length; i++) {// 第一个柱子和最后一个柱子不接雨水if (i==0 || i== height.length - 1) continue;int rHeight = height[i]; // 记录右边柱子的最高高度int lHeight = height[i]; // 记录左边柱子的最高高度for (int r = i+1; r < height.length; r++) {if (height[r] > rHeight) rHeight = height[r];}for (int l = i-1; l >= 0; l--) {if(height[l] > lHeight) lHeight = height[l];}int h = Math.min(lHeight, rHeight) - height[i];if (h > 0) sum += h;}return sum;}
}
单调栈法
class Solution {public int trap(int[] height){int size = height.length;Stack<Integer> stack = new Stack<Integer>();stack.push(0);int sum = 0;for (int index = 1; index < size; index++){int stackTop = stack.peek();if (height[index] < height[stackTop]){stack.push(index);}else if (height[index] == height[stackTop]){// 因为相等的相邻墙,左边一个是不可能存放雨水的,所以pop左边的index, push当前的indexstack.pop();stack.push(index);}else{int heightAtIdx = height[index];while (!stack.isEmpty() && (heightAtIdx > height[stackTop])){int mid = stack.pop();if (!stack.isEmpty()){int left = stack.peek();int h = Math.min(height[left], height[index]) - height[mid];int w = index - left - 1;int hold = h * w;if (hold > 0) sum += hold;stackTop = stack.peek();}}stack.push(index);}}return sum;}
}
本文的分享就到这里了,
我是此林,关注我吧,带你看不一样的世界!