42. 接雨水(单调递减栈)
class Solution { public int trap(int[] height) { int res = 0; Stack<Integer> leftWallStack = new Stack(); int len = height.length; leftWallStack.push(0); for (int i=1;i<len;i++) { int rightWallIndex = i; int stackTopIndex = leftWallStack.peek(); // 栈顶元素要当底部,右边出现比栈顶元素高的,那么它就可以顺利当底部。可以出栈了 while (!leftWallStack.isEmpty() && height[rightWallIndex] > height[stackTopIndex]) { // 出栈 leftWallStack.pop(); if (leftWallStack.isEmpty()) { break; } // 栈的下一个元素要当左边的墙 int leftWallIndex = leftWallStack.peek(); // 左边的墙也要比出栈的容器底部高,才可以接到雨水 if (height[leftWallIndex] > height[stackTopIndex]) { int h = Math.min(height[leftWallIndex], height[rightWallIndex]) - height[stackTopIndex]; int w = rightWallIndex - leftWallIndex - 1; res = res + h*w; } stackTopIndex = leftWallIndex; } // 不管怎么样,用来比较的右边新元素每次都要入栈 leftWallStack.push(i); } return res; }
第二次解
class Solution { public int trap(int[] height) { int sum = 0; Stack<Integer> stack = new Stack(); // 单调递减栈 for (int i=0;i<height.length;i++) { // 栈为空,直接入栈 if (stack.isEmpty()) { stack.push(i); } else { // 比栈顶高度小,直接入栈(因为是单调递减栈) if (height[i] < height[stack.peek()]) { stack.push(i); } else { // 比栈顶高度大就循环出栈接雨水。直到小了,再入栈 while (!stack.isEmpty() && height[i] > height[stack.peek()]) { // 栈顶必出 int top = stack.pop(); // 栈顶出来可能又为空,没法取栈顶下一个了,break if (stack.isEmpty()) { break; } // 栈顶的下一个元素 int topnext = stack.peek(); // 当前 height[i] 和栈顶的下一个元素充当左右墙,栈顶充当底部 int h = Math.min(height[i], height[topnext]) - height[top]; // 注意要多 -1 int w = i-topnext-1; sum = sum + h*w; } // 循环到终于比栈顶大了,入栈 stack.push(i); } } } return sum; } }
剑指 Offer 30. 包含min函数的栈
155. 最小栈
用一个辅助栈 minStack,记录当前最小的元素
push: 如果当前加入的元素比 minStack 栈顶部的小【或等于】,为什么要有等于看代码注释】,就把这个元素也 push 进入 minStack
pop: 从主栈弹出,如果主栈弹出的这个元素和 minStack 相等,说明这个最小的已经走了,minStack 也要 pop 弹出
top:主栈的栈顶(peek)
min: 辅助栈 minStack 的栈顶(peek)
class MinStack { private Stack<Integer> mainStack; private Stack<Integer> minStack; /** initialize your data structure here. */ public MinStack() { mainStack = new Stack(); minStack = new Stack(); } public void push(int x) { mainStack.push(x); // minStack 放的是当前最小,因此只有新入的元素比 minStack 栈顶小,才入栈 // 一定要注意这里要加 = 否则有重复最小元素,只入栈了一个的话,那 pop 的时候这个出栈,之后 minStack.peek() 就会报错 if (minStack.isEmpty() || x <= minStack.peek()) { minStack.push(x); } } public void pop() { int top = mainStack.pop(); // 主栈出栈的时候,如果 minStack 栈顶与主栈栈顶相同,它也要出栈 if (top == minStack.peek()) { minStack.pop(); } } public int top() { return mainStack.peek(); } public int min() { return minStack.peek(); } } /** * Your MinStack object will be instantiated and called as such: * MinStack obj = new MinStack(); * obj.push(x); * obj.pop(); * int param_3 = obj.top(); * int param_4 = obj.min(); */
栈 Stack 是 push 与 pop
队列 Queue 是 offer 与 poll
题解:
我们期望:
维持一个单调递减队列,队列内的元素都是当前窗口内的元素。队列首的元素是最大值,也是当前窗口的最大值
有两个问题:
(1)怎么来维持单调递减呢?也就是说窗口右移的时候,怎么把右边新加入窗口的元素加入到队列中去?
offer 操作:加入队末,前面有比它它小的,就移除,直到队列为空 或 遇到一个比它大的,才推进去
(2)怎么来保证队列中的元素都是窗口内的呢?也就是窗口右移的时候,怎么把左边新移出窗口的元素从队列中去除?
poll 操作:如果是普通的队列,队首的元素直接移除就好了。但是前面的 offer 操作比较特殊,可能窗口最左的元素在前面 offer 的时候因为小于队末新加元素已经被移除了,所以要将队首元素和窗口最左元素比较,如果相等,再将其移除。
这样 peek() 操作,直接获取队首元素,就是当前窗口的最大值。
注意:
- 以上操作都只是操作队列首和队列尾,所以用 LinkedList 比较合适,多用 getFirst() removeFirst() getLast() removeLast() 等操作,不要用下标操作
- 刚开始左右指针 left right 下标都是 0,要左指针不动,右指针右移 k 步完成窗口初始化
- 后面就是 while(right<nums.length)
// 将窗口右边的新元素加入单调递减队列 offer(nums[right++]); // 把窗口左边已经移出窗口的元素,也移出队列 pollIfEquals(nums[left++]); // 单调递减队列,队首元素即为当前窗口的的最大值 res[i++] = peek();
class Solution { LinkedList<Integer> queue; public Solution() { // 用链表类型的 List 来实现 queue = new LinkedList(); } public int[] maxSlidingWindow(int[] nums, int k) { // 最后结果的长度是 nums.length - k + 1 int resLen = nums.length - k + 1; int[] res = new int[resLen]; int left = 0; int right = 0; // 窗口初始化 for (int i=0;i<k;i++) { offer(nums[right++]); } res[0] = peek(); // 窗口右移 int i=1; while(right<nums.length) { // 将窗口右边的新元素加入单调递减队列 offer(nums[right++]); // 按理说应该把窗口左边已经移出窗口的元素,也移出队列 // 但是可能把右边新元素推到队列的过程中,就已经把这个左边的元素移出去了,所以要比较如果相等,才移出,否则 doNothing pollIfEquals(nums[left++]); // 单调递减队列,队首元素即为当前窗口的的最大值 res[i++] = peek(); } return res; } // 将 num 推到队列中,使其维持单调递减 private void offer(int num) { if (queue.isEmpty()) { queue.add(num); return; } // 前面有比它它小的,就移除,直到队列为空 或 遇到一个比它大的,才推进去 while (queue.size() > 0 && queue.getLast() < num) { queue.removeLast(); } queue.add(num); } // 如果队首元素是 n ,则弹出来。如果不是,do nothing private void pollIfEquals(int n) { if (peek() == n) { queue.removeFirst(); } } // 获取队首,因为是单调递减队列,所以也是最大值 private Integer peek() { return queue.getFirst(); } }
739. 每日温度
官方题解
https://leetcode.cn/problems/daily-temperatures/solution/mei-ri-wen-du-by-leetcode-solution/
[73,74,75,71,69,72,76,73]
用单调递减栈来实现:
- 栈为空,或者 当前元素比栈顶小(<=)则进栈
- while(stack不为空 && 当前元素比栈顶大)则出栈
这样,每个元素遇到的让它出栈的哪个元素 就是 它遇到的后面第一个比它温度高的那一天
而让它出栈的那个元素的下标 到 自己的距离就是要求的结果。所以栈里也要存下标。
Stack<List<Integer>> stack 的每个元素是一个 List,List[0] 存温度下标, List[1] 存温度值
res[stackTop.get(0)] = i - stackTop.get(0);
class Solution { public int[] dailyTemperatures(int[] temperatures) { // 单调递减栈, List[0] 存温度下标, List[1] 存温度值 Stack<List<Integer>> stack = new Stack(); int[] res = new int[temperatures.length]; for (int i=0;i<temperatures.length;i++) { // 单调递减栈。栈为空 或 当前元素比栈顶小则进栈 if (stack.isEmpty() || temperatures[i] <= stack.peek().get(1)) { stack.push(Arrays.asList(i, temperatures[i])); continue; } // 注意 while 条件要在【前面】加上一个 !stack.isEmpty() 防止栈已经空掉 while(!stack.isEmpty() && temperatures[i] > stack.peek().get(1)) { List<Integer> stackTop = stack.pop(); // 结果[弹出元素的下标] = 当前元素下标到弹出元素下标的距离 res[stackTop.get(0)] = i - stackTop.get(0); } // 一定要注意这个,当前比栈顶大后,一直到遇到一个比它小的,那么就要把它推入栈里 stack.push(Arrays.asList(i, temperatures[i])); } return res; } }
84. 柱状图中最大的矩形
维持一个单调递增栈(存放下标)
初始化栈底放入一个 -1,后面每次判断栈是否空都是用 stack.peek() != -1
遍历 heigths 数组
每当heights数组新元素比栈顶大或等于,直接入栈
每当heights数组新元素比栈顶小
- topIndex = stack.pop() 栈顶出栈,area = heights[topIndex] * (i-stack.peek()-1)。以此更新结果。
- 直到heights数组新元素比栈顶大或等于,就入栈
- 之前疑惑为什么宽度是 (i-stack.peek()-1) 不是 (i-topIndex) 就好了吗?还有为什么 stack 底部要放一个 -1?这两个问题的答案是一个,见后面
最后栈里可能还会剩下一些元素
- 把此时栈顶的元素作为右边界 rightIndex = stack.peek(),topIndex = stack.pop() 栈顶依次出栈,area = heights[topIndex] * (rightIndex-stack.peek());
为什么宽度是 (i-stack.peek()-1) 不是 (i-topIndex) 就好了吗?还有为什么 stack 底部要放一个 -1?
一般是这样的,高度是栈顶元素 nums[6]=9,因为是单调递增栈,所以宽度顶多是 1(从图中也可以看出)
也就是宽度是:【当前加入的新元素下标 - 栈顶下一个元素的下标 -1】,好像直接用 【当前加入的新元素下标 - 栈顶下标】 替代也可以
但是也有这样的情况:
栈中仅剩元素 nums[3]=2,(想想 nums[3] = 2 让 0~2 的元素都出栈了,然后一直留到 nums[7] = 3 让 4~6 的元素都出栈了)
这个 nums[3]=2 的宽度显然可以达到 8 个,此时用【右边界下标 - 栈顶下标】= 7-3=4 计算是不正确的,只能计算到右半部分的面积,不能计算到之前已经全部出栈过的左半部分的面积
而用 【右边界下标 - 栈顶下一个元素的下标】=7-(-1)=8 则是正确的
class Solution { public int largestRectangleArea(int[] heights) { Stack<Integer> stack = new Stack(); stack.push(-1); int res = 0; for (int i=0;i<heights.length;i++) { // 维持单调递增栈(可以平),比栈顶大或等才入栈 if (stack.peek() == -1 || heights[i] >= heights[stack.peek()]) { // 栈里存的是元素的下标 stack.push(i); } else { // 比栈顶小,就出栈 while (stack.peek() != -1 && heights[i] < heights[stack.peek()]) { int topIndex = stack.pop(); // 面积为 出栈元素的高度 * 到当前元素的距离 int area = heights[topIndex] * (i-stack.peek()-1); if (area>res) { res = area; } } stack.push(i); } } // 最后可能还剩一个单调递增栈,[2(0),4(1),6(2)] int rightIndex = stack.peek(); // 注意这里循环退出的条件不能是 i<stack.size(),因为stack一直在pop,size是在变化的 for (int i=0;stack.peek()!=-1;i++) { int thisIndex = stack.pop(); int area = heights[thisIndex] * (rightIndex - stack.peek()); if (area > res) { res = area; } } return res; } }
394. 字符串解码
class Solution { public String decodeString(String s) { StringBuffer res = new StringBuffer(); StringBuffer curRes = new StringBuffer(); Integer curMulti = 0; // 乘数栈 Stack<Integer> multi_stack = new Stack(); // 结果栈 Stack<String> res_stack = new Stack(); int i=0; while(i<s.length()) { Character c = s.charAt(i); // 是数字的话:读取后续所有连续的数字, if (isDigit(c)) { Integer digits = getDigits2(s, i); // 放到 curMulti 上 curMulti = digits; i+=digits.toString().length(); } // 是字母的话:读取后续所有连续的字母, else if (isLetter(c)) { String letters = getLetters(s, i); // 附加到 curRes 上 curRes.append(letters); i+=letters.length(); } // 是[的话,curMulti curRes 直接推入栈 else if (c == '[') { ++i; // curMulti curRes 直接推入栈 multi_stack.push(curMulti); res_stack.push(curRes.toString()); // curMulti curRes 清零 curMulti=0; curRes=new StringBuffer(); } else if (c == ']') { ++i; // 例:3[a2[c]] // [3,2] --> 2 int multi = multi_stack.pop(); StringBuffer sb = new StringBuffer(); // curRes='c', sb='cc' for (int j=0;j<multi;j++) { sb.append(curRes); } // [a] --> acc curRes = new StringBuffer(res_stack.pop()+sb); } } return curRes.toString(); } private boolean isDigit(char c) { return c>='0' && c<='9'; } private boolean isLetter(char c) { return (c>='a' && c<='z') || (c>='A' && c<='Z'); } private String getDigits(String s, int startIndex) { int i=startIndex; StringBuffer sb = new StringBuffer(); while(i<s.length() && isDigit(s.charAt(i))) { sb.append(s.charAt(i)); i++; } return sb.toString(); } private Integer getDigits2(String s, int startIndex) { int i=startIndex; StringBuffer sb = new StringBuffer(); while(i<s.length() && isDigit(s.charAt(i))) { sb.append(s.charAt(i)); i++; } return Integer.valueOf(sb.toString()); } private String getLetters(String s, int startIndex) { int i=startIndex; StringBuffer sb = new StringBuffer(); while(i<s.length() && isLetter(s.charAt(i))) { sb.append(s.charAt(i)); i++; } return sb.toString(); } private void pushLetters2Stack(Stack<String> stack, String letters) { if (!stack.isEmpty()) { if (isLetter(stack.peek().charAt(0))) { String tops = stack.pop(); stack.push(tops+letters); } } stack.push(letters); } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器