栈是先进后出。
括号匹配是使用栈解决的经典问题。
20.有效的括号
1.字符串为奇数,肯定匹配不上,return false;
2.遍历字符串,当遇到左括号时,往栈中加入一个对应的右括号;
3.当遇到右括号时,判断:如果栈为空或是栈顶元素与当前字符不相等,则return false;否则从栈中弹出一个括号。
4.遍历结束后,判断栈是否为空,为空则true,否则false。
public boolean isValid(String s) {
int n = s.length();
if(n % 2 == 1){
return false;
}
Deque<Character> stack = new LinkedList<Character>();
for(int i=0; i< n; i++){
char c = s.charAt(i);
if(c == '{'){
stack.push('}');
}else if(c == '('){
stack.push(')');
}else if( c== '['){
stack.push(']');
}else if(stack.isEmpty() || stack.peek() != c){
return false;
}else{
stack.pop();
}
}
return stack.isEmpty();
}
- 时间复杂度:O(n)。
- 空间复杂度:O(n+∣Σ∣),其中 Σ 表示字符集,本题中字符串只包含6 种括号,栈中的字符数量为 O(n)。
155.最小栈
要设计一个在常数时间内检索到最小元素的栈。
那么我们需要一个辅助栈,在每个元素a
入栈时把当前栈的最小值 m
存储起来。在这之后无论何时,如果栈顶元素是 a
,我们就可以直接返回存储的最小值 m
。
class MinStack {
Deque<Integer> stack;
Deque<Integer> minStack;
public MinStack() {
stack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
// 注意,存放最小值的初始值一定要给定,否则添加第一个元素的时候,peek()会出错
minStack.push(Integer.MAX_VALUE);
}
public void push(int val) {
stack.push(val);
minStack.push(Math.min(val, minStack.peek()));
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
- 时间复杂度:O(1)。因为栈的插入、删除与读取操作都是O(1) 。
- 空间复杂度:O(n)。
394.字符串解码
题中可能出现括号嵌套的情况,比如 2[a2[bc]],这种情况下我们可以先转化成 2[abcbc],在转化成 abcbcabcbc。因此,把字母、数字和括号看成是独立的 TOKEN,并用栈来维护这些 TOKEN。
思路一:栈
具体的做法是,遍历这个栈:
1.如果当前的字符为数位,解析出一个数字(连续的多个数位)并进栈;
2.如果当前的字符为字母或者左括号,直接进栈;
3.如果当前的字符为右括号,开始出栈,一直到左括号出栈,出栈序列反转后拼接成一个字符串。此时取出栈顶的数字。
4.根据这个次数和字符串构造出新的字符串并进栈。
5.重复,最终将栈中的元素按照从栈底到栈顶的顺序拼接起来,就得到了答案。
思路2:递归
当遇到左括号时,后面的子问题可以再递归重复一遍,函数调用得到的字符串加上当前字符串就是结果。
class Solution {
int ptr;
String src;
public String decodeString(String s) {
src = s;
ptr = 0;
return getString();
}
public int getDigits() {
int ret = 0;
while (ptr < src.length() && Character.isDigit(src.charAt(ptr))) {
ret = ret * 10 + src.charAt(ptr++) - '0';
}
return ret;
}
public String getString() {
// 1.遇到右括号,弹出空串,与字符串组合
if (ptr == src.length() || src.charAt(ptr) == ']') {
// String -> EPS
return "";
}
char cur = src.charAt(ptr);
// 倍数
int repTime = 1;
// 结果字符串
String ret = "";
if (Character.isDigit(cur)) {
// String -> Digits [ String ] String
// 解析 Digits
repTime = getDigits();
// 过滤左括号
++ptr;
// 解析 String
String str = getString();
// 过滤右括号
++ptr;
// 构造字符串
while (repTime-- > 0) {
ret += str;
}
} else if (Character.isLetter(cur)) {
// String -> Char String
// 解析 Char
ret = String.valueOf(src.charAt(ptr++));
}
return ret + getString();
}
}
- 时间复杂度:记解码后得出的字符串长度为 SSS,除了遍历一次原字符串 sss,我们还需要将解码后的字符串中的每个字符都拼接进答案中,故渐进时间复杂度为 O(S+∣s∣),即 O(S)。
- 空间复杂度:若不考虑答案所占用的空间,那么就只剩递归使用栈空间的大小,这里栈空间的使用和递归树的深度成正比,最坏情况下为 O(∣s∣),故渐进空间复杂度为 O(∣s∣)。
好难好难!!!
739. 每日温度
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。
1.单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。
2.单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。
3.如果求一个元素右边第一个更大元素,单调栈的栈顶到栈底是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。
4.主要有三个判断条件。
- 当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
- 当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
- 当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
正向遍历温度列表。对于温度列表中的每个元素 temperatures[i],如果栈为空,则直接将 i 进栈,如果栈不为空,当 temperatures[i] > temperatures[stack.peek()],在res数组中存储 i-stack.peek(),并弹出栈顶元素,重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度,然后将 i 进栈。
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Deque<Integer> stack = new LinkedList<>();
stack.push(0);
for(int i = 1; i<n; i++){
while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]){
res[stack.peek()] = i-stack.peek();
stack.pop();
}
stack.push(i);
}
return res;
}
- 时间复杂度:O(n)。
- 空间复杂度:O(n)。
84. 柱状图中最大的矩形
本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!
- 弹出的都是比自己大的→\rightarrow→确定左边界;
- 被弹出则是发生在第一次遇到比自己小的→\rightarrow→确定右边界
栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度
主要就是分析清楚如下三种情况:
- 情况一:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况
- 情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况
- 情况三:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况
注意:需要新建一个数组,在原height数组的前后,都加了一个元素0,是考虑了以下两种情况:
- 如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 就是单调递减,一直都没有走 情况三 计算结果的哪一步,所以最后输出的就是0了。 如图:
- 如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),rigt(6),但是得不到 left边界。那么为了避免空栈取值,直接跳过了计算结果的逻辑。
public int largestRectangleArea(int[] heights) {
int n=heights.length;
int[] newHeight = new int[n+2];
System.arraycopy(heights,0,newHeight,1,n);
newHeight[0]=newHeight[n+1]=0;
Deque<Integer> stack = new LinkedList<>();
stack.push(0);
int res = 0;
for(int i=1; i< n+2; i++){
while(!stack.isEmpty() && newHeight[i] < newHeight[stack.peek()]){
int mid=stack.pop();
int h = newHeight[mid];
int w = i-stack.peek()-1;
res=Math.max(res, w * h);
}
stack.push(i);
}
return res;
- 时间复杂度:O(N)
- 空间复杂度:O(N)