栈专题—栈基础

通关进度

题目 说明
数组栈 通关
链表栈 通关

参考资料

一网打尽队栈结构

数组栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class AStack<T> {

// 栈结构
private T[] stack;
// 栈顶指针
private int top;

//构造函数
public AStack() {

// 以下行会导致编译时错误
// private T[] array = new T[10]; 泛型擦除
this.stack = (T[]) new Object[10];
}

// 判空
public boolean isEmpty() {
return top == 0;
}

// 取栈顶元素
public T peek() {
T t = null;
if (top > 0) {
t = stack[top - 1];
}
return t;
}

// 出栈
public T pop() {
T t = peek();
if (top > 0) {
stack[top - 1] = null;
top--;
}
return t;
}

// 入栈
public void push(T t) {
expandCapacity(top + 1);
stack[top++] = t;
}

// 扩容
public void expandCapacity(int size) {
int length = stack.length;
if (size > length) {
size = size * 3 / 2 + 1; // 每次扩大 50%
stack = Arrays.copyOf(stack, size);
}
}
}

链表栈

链表实现栈:在头节点进行插入和删除节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class LStack<T> {

// 链表结构
class Node<T> {
public T data;
public Node next;
}

// 头指针
public Node<T> head;

public LStack() {
this.head = null;
}

// 入栈
public void push(T t) {
if (t == null) {
throw new NullPointerException("参数不能为空");
}
if (head == null) {
head = new Node<>();
head.data = t;
head.next = null;
} else {
Node<T> temp = head;
head = new Node<>();
head.data = t;
head.next = temp;
}
}

// 出栈
public T pop() {
if (head == null) {
return null;
}
T t = head.data;
head = head.next;
return t;
}

// 取栈顶元素
public T peek() {
if (head == null) {
return null;
}

return head.data;
}

// 判空
public boolean isEmpty() {
return head == null;
}
}

栈专题—栈强化

通关进度

题目 说明
括号匹配问题 通关
最小栈 通关
最大栈 通关

括号匹配问题

20. 有效的括号

问题

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

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

算法思路

  1. 当遇到一个左括号时,会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此可以将这个左括号放入栈顶。
  2. 当遇到一个右括号时,需要将一个相同类型的左括号闭合。此时,可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。
    为快速判断括号的类型,可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。

在遍历结束后,如果栈中没有左括号,说明将字符串 s 中的所有左括号闭合,返回 True,否则返回 False。

注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 False,省去后续的遍历判断过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean isValid(String s) {
// 不是偶数,直接 pass
if (s.length() % 2 == 1) {
return false;
}
Map<Character, Character> pairs = new HashMap<>();
pairs.put(')', '(');
pairs.put('}', '{');
pairs.put(']', '[');

Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (pairs.containsKey(ch)) {
if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {
return false;
}
stack.pop();
} else {
stack.push(ch);
}
}
return stack.isEmpty();
}

扩展

LeetCode 22.括号生成
LeetCode 32.最长有效括号
LeetCode 301.删除无效的括号
LeetCode 856.括号的分数

最小栈

155. 最小栈

问题

【LeetCode 155】:设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.

注意

题目要求在常数时间内获得栈最小值,因此不能在 getMin() 时再计算最小值

栈的性质

对于栈来说,如果一个元素 a 在入栈时,栈里有其它的元素 b, c, d,那么无论这个栈在之后经历了什么操作,只要 a 在栈中,b, c, d 就一定在栈中,因为在 a 被弹出之前,b, c, d 不会被弹出。

因此,在操作过程中的任意一个时刻,只要栈顶的元素是 a,那么我们就可以确定栈里面现在的元素一定是 a, b, c, d

那么,我们可以在每个元素 a 入栈时把当前栈的最小值 m 存储起来。在这之后无论何时,如果栈顶元素是 a,我们就可以直接返回存储的最小值 m

只需要设计一个数据结构,使得每个元素 a 与其相应的最小值 m 时刻保持一一对应,可以使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。

  • 当一个元素要入栈时,取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;

  • 当一个元素要出栈时,把辅助栈的栈顶元素也一并弹出;

  • 在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class MinStack {

// 存储原始元素
private Deque<Integer> stack;
// 存储对应最小值
private Deque<Integer> minStack;


public MinStack() {
stack = new LinkedList<>();
minStack = new LinkedList<>();
minStack.push(Integer.MAX_VALUE);
}

// 入栈
public void push(int x) {
// 压入原始值
stack.push(x);
// 叫小部分压入栈
minStack.push(Math.min(x, minStack.peek()));
}

// 出栈
public void pop() {
stack.pop();
minStack.pop();
}

// 顶部元素
public int top() {
return stack.peek();
}

// getMin
public int getMin() {
return minStack.peek();
}

}

最大栈

问题

【LeetCode 716】:设计一个支持 pushpoptop 操作,并能在常数时间内检索到最大元素的栈。

实现 MaxStack 类:

  • MaxStack() 初始化栈对象
  • push(x) 将元素 x 压入栈中
  • pop() 移除栈顶元素并返回这个值
  • top() 返回栈顶元素
  • peekMax() 返回栈中最大元素
  • popMax() 返回栈中最大的元素,并将其删除
      如果有多个最大元素,只要删除最靠近栈顶的那个。
    
1
2
3
4
5
6
7
8
9
10
MaxStack stack = new MaxStack();
stack.push(5);
stack.push(1);
stack.push(5);
stack.top(); -> 5
stack.popMax(); -> 5
stack.top(); -> 1
stack.peekMax(); -> 5
stack.pop(); -> 1
stack.top(); -> 5

peekMax

  • 使用新栈存储每个位置到栈底的最大元素
  • 例如,stack = [2, 1, 5, 3, 9]maxStack = [2, 2, 5, 5, 9]
  • push(x):仅需将 maxStack 的栈顶元素和 XX 的最大值压栈
  • pop():仅需将 maxStack 出栈

    popMax

  • 由于知道当前栈中最大元素值,因此直接将两个栈同时出栈,并存储 stack 出栈过程所有值

  • 当某时刻,stack 出栈元素等于当前栈中最大的元素值时,便找到最大元素
  • 此时将之前 stack 出栈的元素重新入栈,并同步更新 maxStack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 public class MaxStack {

// 存储原始元素
private Deque<Integer> stack;
// 存储对应最大元素
private Deque<Integer> maxStack;

public MaxStack() {
this.stack = new LinkedList<>();
this.maxStack = new LinkedList<>();
}

public void push(int x) {
int max = maxStack.isEmpty() ? x : maxStack.peek();
maxStack.push(max > x ? max : x);
stack.push(x);
}

public int pop() {
maxStack.pop();
return stack.pop();
}

public int top() {
return stack.peek();
}

public int peekMax() {
return maxStack.peek();
}

public int popMax() {
int max = peekMax();
Deque<Integer> temp = new LinkedList<>();
while (top() != max) {
temp.push(pop());
}
pop();
while (!temp.isEmpty()) {
push(temp.pop());
}
return max;
}
}

栈专题—表达式栈

通关进度

题目 说明
计算器实现 通关
逆波兰表达式 通关

计算器问题

227. 基本计算器 II

问题

【LeetCode 227】:给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。
你可以假设给定的表达式总是有效的。所有中间结果将在 $[-2^{31}, 2^{31} - 1]$ 的范围内。

由于乘除优先于加减计算,因此不妨考虑先进行所有乘除运算,并将这些乘除运算后的整数值放回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。

基于此,可以用一个栈,保存进行乘除运算后的值。对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。

具体来说,遍历字符串 s,并用变量 preSign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 preSign 来决定计算方式:

  • 加号:将数字压入栈;
  • 减号:将数字的相反数压入栈;
  • 乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。

代码实现中,若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾。处理完该数字后,更新 preSign 为当前遍历的字符。

遍历完字符串 s 后,将栈中元素累加,即为该字符串表达式的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public int calculate(String s) {

Deque<Integer> stack = new ArrayDeque<Integer>();
char preSign = '+';
int num = 0;
int n = s.length();
for (int i = 0; i < n; ++i) {
if (Character.isDigit(s.charAt(i))) {
num = num * 10 + s.charAt(i) - '0';
}
if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != ' ' || i == n - 1) {
switch (preSign) {
case '+':
stack.push(num);
break;
case '-':
stack.push(-num);
break;
case '*':
stack.push(stack.pop() * num);
break;
default:
stack.push(stack.pop() / num);
}
preSign = s.charAt(i);
num = 0;
}
}
int ans = 0;
while (!stack.isEmpty()) {
ans += stack.pop();
}
return ans;
}

逆波兰表达式

150. 逆波兰表达式求值

问题

【LeetCode 150】:给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。

注意:

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

逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:

  • 如果遇到操作数,则将操作数入栈;

  • 如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。

整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<Integer>();
int n = tokens.length;
for (int i = 0; i < n; i++) {
String token = tokens[i];
if (isNumber(token)) {
stack.push(Integer.parseInt(token));
} else {
int num2 = stack.pop();
int num1 = stack.pop();
switch (token) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
default:
}
}
}
return stack.pop();
}

public boolean isNumber(String token) {
return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
}

说明

前缀表达式的运算符位于操作数之前,中缀和后缀同理,其实本质对应二叉树前/中/后序遍历方式