一、知识

栈是一种”操作受限”的线性表,只允许在一端插入和删除数据。后进者先出,先进者后出,这就是典型的“栈”结构。

栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。

栈的空间复杂度为O(1)(这里需要n个空间保存数据并不是说空间复杂度即为O(n),而是该n个空间是必须的无法省略),不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度都是 O(1) 。

Java中栈的实现:Vector类具有动态扩容和随机访问的特性,因此,继承了Vector类的Stack也同样具有这些特性,这恰好违背了Stack数据结构的设计原理,正因为如此,Java中的Stack一直被认为是糟糕的实现,官方也将Stack标志为“弃用”。官方推荐使用Deque(双端队列)接口来实现Stack

二、应用

1.有效的括号

使用map存储配对的字符,当字符为map中的key时字符入栈,当字符不为map的key时字符出栈进行对比。

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
public static boolean isValid(String s) {
int length = s.length();
//如果字符串长度不为偶数直接结束
if (length % 2 != 0) {
return false;
}
//map存储配对字符
Map<Character, Character> map = new HashMap<>() {
{
put('(', ')');
put('[', ']');
put('{', '}');
}
};
//deque实现栈
Deque<Character> stack = new LinkedList<>();
for (int i = 0; i < length; i++) {
char elt = s.charAt(i);
//判断map中是否存在key
if (map.containsKey(elt)) {
stack.push(elt);
continue;
}
//判断是否栈空,不为空再比较字符是否配对
if (stack.isEmpty() || map.get(stack.pop()) != elt) {
return false;
}
}
//判断是否栈空,字符是否配对完
return stack.isEmpty();
}

2.最小栈

使用两个栈,一个栈存储数据,一个栈存储最小值这样在常数时间内能找到最小值。

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
class MinStack {

Deque<Integer> stack;
//存储最小值
Deque<Integer> minStack;

public MinStack() {
stack = new LinkedList();
minStack = new LinkedList();
//添加入最大值与添加的数据进行大小比较
minStack.push(Integer.MAX_VALUE);
}

public void push(int val) {
stack.push(val);
//将值与最小值中的栈顶值比较,添加较小的值
minStack.push(Math.min(minStack.peek(), val));
}

public void pop() {
stack.pop();
minStack.pop();
}

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

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

}
}

3.用栈实现队列

使用两个栈来实现队列,一个栈(tail)实现数据的入队,另外一个栈(head)实现数据的出队,当出队数据为空时判断tail栈是否存在数据,将数据添加至head栈实现数据的正序。

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
public class MyQueue {
Deque<Integer> head;
Deque<Integer> tail;

public MyQueue() {
head = new LinkedList();
tail = new LinkedList();
}

public void push(int x) {
tail.push(x);
}

public int pop() {
exchange();
return head.pop();
}

public int peek() {
exchange();
return head.peek();
}

public boolean empty() {
return (head.isEmpty() && tail.isEmpty());
}

public void exchange() {
//判断head栈是否存为空
if (!head.isEmpty()) {
return;
}
//判断tail栈是否为空
if (!tail.isEmpty()) {
//进行数据的转移
while (!tail.isEmpty()) {
head.push(tail.pop());
}
}
}

}

4.基本计算器

将括号前的符号存入栈中,括号中的计算都要变换符号,当括号结束再出栈变为括号之前的符号。

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
class Solution {
public int calculate(String s) {
Deque<Integer> stack = new ArrayDeque<>();
//最开始的符号
stack.push(1);
int ret = 1;
int sum = 0;
int i = 0;
while (i < s.length()) {
char elt = s.charAt(i);
switch (elt) {
case ' ':
i++;
break;
case '+':
ret = stack.peek();
i++;
break;
case '-':
ret = -stack.peek();
i++;
break;
case '(':
stack.push(ret);
i++;
break;
case ')':
stack.pop();
i++;
break;
default:
long num = 0;
//循环遍历找出完整的数字
while (i < s.length() && Character.isDigit(s.charAt(i))) {
num = num * 10 + s.charAt(i) - '0';
i++;
}
sum += ret * num;
}
}
return sum;
}
}

5.逆波兰表达式求值

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果

  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

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
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque();
int length = tokens.length;
for (int i = 0; i < length; i++) {
String temp = tokens[i];
//判断是否是数字
if (!isNumber(temp)) {
stack.push(Integer.valueOf(temp));
} else {
//不为数字则进行运算,运算结果入栈
int num1 = stack.pop();
int num2 = stack.pop();
switch (temp) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num2 - num1);
break;
case "/":
stack.push(num2 / num1);
break;
case "*":
stack.push(num1 * num2);
break;
}
}
}
//将栈中的最终结果fan
return stack.pop();
}

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

6.下一个更大元素 I

单调栈+哈希表:首先从后遍历数组nums2,单调栈用于存储当前位置后比当前元素大的元素,每次进行比较,当前元素小于栈内元素则当前元素后的最大元素为栈顶元素,否则将元素出栈找到最大的元素,不存在则为-1,使用map可以在遍历nums1数组时找到nums2对应位置的下个最大元素(也可以存储下标代替值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int length = nums1.length;
int[] arr = new int[length];
//单调栈
Deque<Integer> stack = new ArrayDeque<>();
Map<Integer, Integer> map = new HashMap<>();
for (int i = nums2.length - 1; i >= 0; i--) {
int temp = nums2[i];
//判断栈是否为空,不为空比较大小
while (!stack.isEmpty() && stack.peek() <= temp) {
stack.pop();
}
//不为空栈顶元素为最大元素,否则为-1
map.put(temp, stack.isEmpty() ? -1 : stack.peek());
stack.push(temp);
}
//遍历nums1数组
for (int i = 0; i < length; i++) {
arr[i] = map.get(nums1[i]);
}
return arr;
}
}

7.最长有效括号

栈:将 ( 的下标压入栈中,当遇到 ) 时将出栈,通过栈是否为空判断是否为该 ) 是否存在对应的 (

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int longestValidParentheses(String s) {
//栈
Deque<Integer> stack = new ArrayDeque();
int max = 0;
//防止第一个为 ),出栈出现异常
stack.push(-1);
for (int i = 0; i < s.length(); i++) {
char elt = s.charAt(i);
if (elt == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) {
stack.push(i);
} else {
max = Math.max(max, i - stack.peek());
}
}
}
return max;
}
}

https://pursuemilk.github.io/2023/01/23/栈/
作者
PursueMilk
发布于
2023年1月23日
许可协议