力扣top100(day04-04)--栈
本文为力扣TOP100刷题笔记
笔者根据数据结构理论加上最近刷题整理了一套 数据结构理论加常用方法以下为该文章:
力扣外传之数据结构(一篇文章搞定数据结构)
20. 有效的括号
class Solution {public boolean isValid(String s) {int n = s.length();if (n % 2 == 1) { // 如果字符串长度为奇数,直接返回falsereturn false;}// 使用HashMap存储右括号和对应的左括号Map<Character, Character> pairs = new HashMap<Character, Character>() {{put(')', '(');put(']', '[');put('}', '{');}};// 使用栈来辅助判断Deque<Character> stack = new LinkedList<Character>();for (int i = 0; i < n; i++) {char ch = s.charAt(i);if (pairs.containsKey(ch)) { // 如果是右括号if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {return false; // 栈为空或栈顶不匹配,返回false}stack.pop(); // 匹配成功,弹出栈顶的左括号} else { // 如果是左括号stack.push(ch); // 压入栈中}}return stack.isEmpty(); // 栈为空说明全部匹配成功}
}
关键点说明:
奇数长度检查:
如果字符串长度为奇数,直接返回
false
,因为括号必须成对出现。括号匹配映射:
使用
HashMap
存储右括号和对应的左括号,方便快速查找。栈的使用:
遇到左括号时,压入栈中。
遇到右括号时,检查栈顶的左括号是否匹配:
如果栈为空或不匹配,返回
false
。如果匹配,弹出栈顶的左括号。
最终检查:
遍历完字符串后,如果栈为空,说明所有括号都正确匹配;否则,说明有未匹配的左括号。
示例:
输入:
s = "()[]{}"
执行过程:
'('
是左括号,压入栈 →stack = ['(']
。
')'
是右括号,栈顶'('
匹配 → 弹出'('
→stack = []
。
'['
是左括号,压入栈 →stack = ['[']
。
']'
是右括号,栈顶'['
匹配 → 弹出'['
→stack = []
。
'{'
是左括号,压入栈 →stack = ['{']
。
'}'
是右括号,栈顶'{'
匹配 → 弹出'{'
→stack = []
。栈为空,返回
true
。输入:
s = "([)]"
执行过程:
'('
压入栈 →stack = ['(']
。
'['
压入栈 →stack = ['[', '(']
。
')'
是右括号,栈顶'['
不匹配 → 返回false
。时间复杂度 & 空间复杂度
时间复杂度:
O(n)
,只需遍历一次字符串。空间复杂度:
O(n)
,最坏情况下栈的大小为n
(例如全是左括号)。总结
该算法利用栈的先进后出特性,高效地验证括号字符串的有效性。通过维护一个栈来匹配括号,确保每个右括号都能正确闭合最近的左括号。
155. 最小栈
import java.util.Deque;
import java.util.LinkedList;/*** 最小栈实现,支持常数时间内完成 push、pop、top 和 getMin 操作*/
class MinStack {// 主栈:存储所有压入的元素private Deque<Integer> xStack;// 辅助栈:存储当前栈中的最小值,栈顶始终为当前最小值private Deque<Integer> minStack;/*** 构造函数:初始化两个栈* minStack 初始压入 Integer.MAX_VALUE 作为哨兵值,* 避免后续空栈判断*/public MinStack() {xStack = new LinkedList<>();minStack = new LinkedList<>();minStack.push(Integer.MAX_VALUE); // 初始化哨兵值}/*** 压入元素到栈中* @param x 待压入的元素* 1. 将元素压入主栈* 2. 比较当前元素与辅助栈顶的最小值,压入更小的那个* (维护辅助栈的栈顶始终是当前最小值)*/public void push(int x) {xStack.push(x);// 比较并压入当前最小值minStack.push(Math.min(minStack.peek(), x));}/*** 弹出栈顶元素* 1. 主栈弹出栈顶元素* 2. 辅助栈同步弹出栈顶(维护最小值状态)* 注意:题目保证 pop 操作时栈不为空*/public void pop() {xStack.pop();minStack.pop();}/*** 获取栈顶元素(不弹出)* @return 栈顶元素*/public int top() {return xStack.peek();}/*** 获取当前栈中的最小值* @return 最小值* 直接返回辅助栈的栈顶元素*/public int getMin() {return minStack.peek();}
}/*** 使用示例:* MinStack obj = new MinStack();* obj.push(val);* obj.pop();* int param_3 = obj.top();* int param_4 = obj.getMin();*//*** Your MinStack object will be instantiated and called as such:* MinStack obj = new MinStack();* obj.push(val);* obj.pop();* int param_3 = obj.top();* int param_4 = obj.getMin();*/
关键点注释说明:
双栈设计:
xStack
作为主栈存储所有数据
minStack
作为辅助栈同步存储当前最小值哨兵值初始化:
minStack
初始压入Integer.MAX_VALUE
作为保护值确保第一次
push
时可以直接比较,避免空栈判断push操作:
主栈正常压入
辅助栈压入
Math.min(当前最小值, 新元素)
,保证栈顶始终是最小值pop操作:
双栈同步弹出,保持状态一致
时间复杂度:
所有操作都是直接访问栈顶,保证 O(1) 时间复杂度
线程安全:
注释中未体现,但实际使用时应注意:
该实现不是线程安全的
多线程环境下需要使用并发集合或加锁
在
MinStack
的实现中,minStack.peek()
是用于获取辅助栈栈顶元素的方法调用,它直接返回当前栈中的最小值而不移除该元素。这个实现完美满足了最小栈的所有功能需求,且每个操作都保持在常数时间复杂度内完成。辅助栈的设计是核心思想,通过空间换时间的方式优化了最小值的获取效率。
394. 字符串解码
class Solution {int ptr; // 全局指针,用于遍历字符串public String decodeString(String s) {LinkedList<String> stk = new LinkedList<String>(); // 使用链表模拟栈ptr = 0; // 初始化指针while (ptr < s.length()) {char cur = s.charAt(ptr); // 当前字符if (Character.isDigit(cur)) {// 如果当前字符是数字,获取完整的数字并压入栈String digits = getDigits(s);stk.addLast(digits);} else if (Character.isLetter(cur) || cur == '[') {// 如果当前字符是字母或左括号,直接压入栈stk.addLast(String.valueOf(s.charAt(ptr++))); } else {// 当前字符是右括号,开始处理栈中的内容++ptr;LinkedList<String> sub = new LinkedList<String>();// 弹出栈顶元素直到遇到左括号,这些元素构成需要重复的子串while (!"[".equals(stk.peekLast())) {sub.addLast(stk.removeLast());}Collections.reverse(sub); // 反转子串列表,因为栈是后进先出// 弹出左括号stk.removeLast();// 弹出数字,表示子串的重复次数int repTime = Integer.parseInt(stk.removeLast());StringBuffer t = new StringBuffer();String o = getString(sub); // 将子串列表转为字符串// 构造重复后的字符串while (repTime-- > 0) {t.append(o);}// 将构造好的字符串压入栈stk.addLast(t.toString());}}// 栈中剩余的就是解码后的字符串return getString(stk);}// 从字符串中提取连续的数字public String getDigits(String s) {StringBuffer ret = new StringBuffer();while (Character.isDigit(s.charAt(ptr))) {ret.append(s.charAt(ptr++));}return ret.toString();}// 将字符串列表拼接成一个字符串public String getString(LinkedList<String> v) {StringBuffer ret = new StringBuffer();for (String s : v) {ret.append(s);}return ret.toString();}
}
关键点说明
栈的使用:
使用栈来临时存储数字、字母和括号,遇到右括号时开始处理栈中的内容。
栈中存储的元素可以是数字、字母或构造好的字符串。
处理数字:
当遇到数字时,调用
getDigits
方法提取完整的数字(可能有多位),并将其压入栈。处理字母和左括号:
直接压入栈,指针向后移动。
处理右括号:
弹出栈顶元素直到遇到左括号,这些元素构成需要重复的子串。
反转子串列表,因为栈是后进先出的结构。
弹出左括号后,栈顶的数字表示子串的重复次数。
构造重复后的字符串并压入栈。
最终结果:
遍历完字符串后,栈中剩余的元素拼接起来就是解码后的字符串。
示例演示
输入:
s = "3[a2[c]]"
执行过程:
遇到
3
,压入栈 →stk = [3]
遇到
[
,压入栈 →stk = [3, "["]
遇到
a
,压入栈 →stk = [3, "[", "a"]
遇到
2
,压入栈 →stk = [3, "[", "a", 2]
遇到
[
,压入栈 →stk = [3, "[", "a", 2, "["]
遇到
c
,压入栈 →stk = [3, "[", "a", 2, "[", "c"]
遇到
]
:
弹出直到
[
→sub = ["c"]
反转
sub
→sub = ["c"]
弹出
[
→stk = [3, "[", "a", 2]
弹出数字
2
→repTime = 2
构造字符串
t = "cc"
压入
t
→stk = [3, "[", "a", "cc"]
遇到
]
:
弹出直到
[
→sub = ["a", "cc"]
反转
sub
→sub = ["cc", "a"]
(注意顺序)弹出
[
→stk = [3]
弹出数字
3
→repTime = 3
构造字符串
t = "accaccacc"
压入
t
→stk = ["accaccacc"]
最终结果:
"accaccacc"
时间复杂度
时间复杂度:
O(n)
,其中n
是解码后字符串的长度。每个字符最多入栈和出栈一次。空间复杂度:
O(n)
,栈的空间占用最多为n
。总结
该算法通过栈结构有效地处理了嵌套的编码字符串,利用递归的思想(通过栈模拟)逐步展开嵌套的编码部分。关键在于遇到右括号时的处理逻辑,即如何从栈中提取子串和重复次数,并重新构造字符串后压回栈中。这种方法保证了高效性和正确性。
739. 每日温度
class Solution {public int[] dailyTemperatures(int[] temperatures) {// 获取温度数组的长度int length = temperatures.length;// 初始化结果数组,默认值为0(即没有更高温度时返回0)int[] ans = new int[length];// 使用双端队列实现单调栈,存储温度数组的索引Deque<Integer> stack = new LinkedList<Integer>();// 遍历温度数组for (int i = 0; i < length; i++) {int currentTemp = temperatures[i];// 当栈不为空且当前温度高于栈顶索引对应的温度时while (!stack.isEmpty() && currentTemp > temperatures[stack.peek()]) {// 弹出栈顶索引(即之前未找到更高温度的那天)int prevDayIndex = stack.pop();// 计算等待天数 = 当前天数 - 之前的天数ans[prevDayIndex] = i - prevDayIndex;}// 将当前天数的索引入栈(等待后续更高温度)stack.push(i);}// 返回结果数组return ans;}
}
关键注释说明:
结果数组初始化:
ans
数组长度与输入相同,默认值为0,自动处理没有更高温度的情况单调栈的作用:
栈中存储的是温度数组的索引,而非温度值本身
保持栈中索引对应的温度是单调递减的(从栈底到栈顶)
核心处理逻辑:
当遇到比栈顶温度高的当前温度时:
弹出栈顶索引(之前未找到更高温度的天数)
计算等待天数(当前索引 - 之前索引)
将结果存入对应位置
这个循环会处理所有比当前温度低的栈中天数
索引入栈:
每个天数索引都会被压入栈中
这些索引将在后续遍历中被处理(当遇到更高温度时)
边界情况自动处理:
如果某天之后没有更高温度,结果保持初始值0
空输入情况也能正确处理(返回空数组)
算法特点:
时间复杂度:O(n),每个元素最多入栈和出栈一次
空间复杂度:O(n),最坏情况下需要存储所有索引
稳定性:能正确处理各种边界情况(单调递增/递减/平缓温度序列)
让我们通过一个具体的例子来详细演示这个算法的运行过程。假设输入的温度数组是:
text
temperatures = [73, 74, 75, 71, 69, 72, 76, 73]我们将逐步跟踪算法的执行过程,包括栈的变化和结果数组的更新。
初始状态
ans = [0, 0, 0, 0, 0, 0, 0, 0]
stack = []
(空栈)逐步执行
i = 0, temperature = 73
栈为空,直接压入索引0
stack = [0]
ans = [0, 0, 0, 0, 0, 0, 0, 0]
i = 1, temperature = 74
74 > temperatures[0]=73:
弹出0,计算ans[0] = 1 - 0 = 1
stack = []
压入1
stack = [1]
ans = [1, 0, 0, 0, 0, 0, 0, 0]
i = 2, temperature = 75
75 > temperatures[1]=74:
弹出1,计算ans[1] = 2 - 1 = 1
stack = []
压入2
stack = [2]
ans = [1, 1, 0, 0, 0, 0, 0, 0]
i = 3, temperature = 71
71 <= temperatures[2]=75:
直接压入3
stack = [2, 3]
ans = [1, 1, 0, 0, 0, 0, 0, 0]
i = 4, temperature = 69
69 <= temperatures[3]=71:
直接压入4
stack = [2, 3, 4]
ans = [1, 1, 0, 0, 0, 0, 0, 0]
i = 5, temperature = 72
72 > temperatures[4]=69:
弹出4,计算ans[4] = 5 - 4 = 1
stack = [2, 3]
72 > temperatures[3]=71:
弹出3,计算ans[3] = 5 - 3 = 2
stack = [2]
72 <= temperatures[2]=75:
压入5
stack = [2, 5]
ans = [1, 1, 0, 2, 1, 0, 0, 0]
i = 6, temperature = 76
76 > temperatures[5]=72:
弹出5,计算ans[5] = 6 - 5 = 1
stack = [2]
76 > temperatures[2]=75:
弹出2,计算ans[2] = 6 - 2 = 4
stack = []
压入6
stack = [6]
ans = [1, 1, 4, 2, 1, 1, 0, 0]
i = 7, temperature = 73
73 <= temperatures[6]=76:
直接压入7
stack = [6, 7]
ans = [1, 1, 4, 2, 1, 1, 0, 0]
最终结果
text
ans = [1, 1, 4, 2, 1, 1, 0, 0]结果解释
ans[0]=1:第0天(73)后第1天(74)温度更高
ans[1]=1:第1天(74)后第2天(75)温度更高
ans[2]=4:第2天(75)后第6天(76)温度更高(间隔4天)
ans[3]=2:第3天(71)后第5天(72)温度更高
ans[4]=1:第4天(69)后第5天(72)温度更高
ans[5]=1:第5天(72)后第6天(76)温度更高
ans[6]=0:第6天(76)之后没有更高温度
ans[7]=0:第7天(73)是最后一天,没有更高温度
这个演示清晰地展示了单调栈如何高效地跟踪需要等待更高温度的天数,并在遇到更高温度时立即更新之前所有可以确定结果的天数。