数据结构之“栈”

顺序栈

顺序栈是指利用一组地址连续的存储单元(即数组)依次存放自栈底至栈顶的数据元素。在顺序栈中,我们只需要关注两个物理量:

  • 数组 (Array):用来承载数据的容器。
  • 栈顶指针 (top):本质上是一个索引(Index)。它始终指向当前栈中最新插入的元素位置。

顺序栈利用数组的连续性,剔除了链表节点频繁申请内存的开销。它虽然只能在一端操作,但其极致的读写速度,让它成为了递归调用、表达式求值(如计算器逻辑)以及浏览器后退功能的不二之选。

动态扩容的顺序栈核心代码:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* @author KJ
* @description 动态扩容的顺序栈
*/
public class MyArrayStack {
private int[] data; // 存储容器
private int top; // 栈顶指针(始终指向下一个待插入的位置,初始为 0)
private int capacity; // 当前容量

private static final int DEFAULT_CAPACITY = 10;

public MyArrayStack() {
this(DEFAULT_CAPACITY);
}

public MyArrayStack(int initCapacity) {
this.capacity = initCapacity;
this.data = new int[capacity];
this.top = 0;
}

private void resize(int newCapacity) {
int[] newData = new int[newCapacity];
System.arraycopy(data, 0, newData, 0, top);
data = newData;
capacity = newCapacity;
}

private boolean isEmpty() {
return top == 0;
}

/**
* 入栈 - O(1) 均摊
*/
public void push(int val) {
// 检查是否满员,满则扩容
if (top == capacity) {
resize(capacity * 2);
}
// 赋值并移动指针
data[top++] = val;
}

/**
* 出栈 - O(1)
*/
public int pop() {
if (isEmpty()) throw new EmptyStackException();

// 拿到栈顶元素
int val = data[--top];

// 缩容优化:当元素只占容量的 1/4 时,缩小一半,节省空间
if (top > 0 && top == capacity / 4 && capacity / 2 >= DEFAULT_CAPACITY) {
resize(capacity / 2);
}
return val;
}

/**
* 查看栈顶元素
*/
public int peek() {
if (isEmpty()) throw new EmptyStackException();
return data[top - 1];
}

/**
* 栈大小
*/
public int size() {
return top;
}

/**
* 遍历打印(top-> bottom)
*/
public void print() {
for (int i = top - 1; i >= 0; i--) {
System.out.print(data[i] + " ");
}
System.out.println();
}

public static void main(String[] args) {
MyArrayStack stack = new MyArrayStack(2); // 初始容量设小点方便观察
stack.push(10);
stack.push(20);
stack.push(30); // 此时会触发扩容
stack.print(); // 30 20 10

System.out.println("Pop: " + stack.pop()); // Pop: 30
System.out.println("Peek: " + stack.peek()); // Peek: 20
stack.print(); // 20 10
}
}


链式栈

在顺序栈统治了高性能计算(如 JVM 虚拟机栈、编译器底层)的天下时,链式栈依然在某些特定领域有着不可替代的地位。它的核心优势在于 “无限增长” 和 “零内存碎片浪费”。以下是链式栈的三个典型实际应用场景:

  • 撤销(Undo/Redo)操作的 “无限历史记录”
  • 符号表管理:比如实现一个计算器
  • 深度优先搜索(DFS)与回溯算法
四则运算过程模拟
表达式: 3 + 5 * 2 - 6 / 2
数字栈 (Num Stack)
符号栈 (Op Stack)
准备就绪,点击开始
步进间隔:

链式栈小计算器核心代码:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* @author KJ
* @description 使用链式栈造一个简单的计算器
*/
public class MyStackCalculator {
private static class LinkedStack<T> {
private class Node {
T data;
Node next;

Node(T data) {
this.data = data;
}
}

private Node top; // 始终指向当前栈顶节点

public void push(T val) {
Node node = new Node(val);
node.next = top;
top = node;
}

public T pop() {
if (top == null) throw new EmptyStackException();
T val = top.data;
top = top.next;
return val;
}

public T peek() {
return (top == null) ? null : top.data;
}

public boolean isEmpty() {
return top == null;
}
}

/**
* 计算核心方法
*/
public int calculate(String exp) {
LinkedStack<Integer> numStack = new LinkedStack<>();
LinkedStack<Character> opStack = new LinkedStack<>();
for (int i = 0; i < exp.length(); i++) {
char c = exp.charAt(i);
if (Character.isDigit(c)) {
// 处理多位数(如 "12")
int num = 0;
while (i < exp.length() && Character.isDigit(exp.charAt(i))) {
num = num * 10 + (exp.charAt(i) - '0');
i++;
}
i--; // 回退一步,补偿外层循环的 i++
numStack.push(num);
} else if (isOperator(c)) {
// 只要当前符号优先级 <= 栈顶符号,就先计算
while (!opStack.isEmpty() && priority(c) <= priority(opStack.peek())) {
process(numStack, opStack);
}
opStack.push(c);
}
}
// 表达式遍历完,把栈里剩下的算完
while (!opStack.isEmpty()) {
process(numStack, opStack);
}
return numStack.pop();
}

private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}

private int priority(char op) {
if (op == '*' || op == '/') return 2;
if (op == '+' || op == '-') return 1;
return 0;
}

private void process(LinkedStack<Integer> nums, LinkedStack<Character> ops) {
int b = nums.pop(); // 注意:先弹出的是右操作数
int a = nums.pop();
char op = ops.pop();
switch (op) {
case '+': nums.push(a + b); break;
case '-': nums.push(a - b); break;
case '*': nums.push(a * b); break;
case '/': nums.push(a / b); break;
}
}

public static void main(String[] args) {
MyStackCalculator calc = new MyStackCalculator();
String expression = "3 + 5*2-6/2"; // 有干扰符也无所谓
System.out.println(calc.calculate(expression)); // 10
}
}


单调栈

单调栈不是一种新的物理结构,而是一种操作策略。它要求栈内的元素必须保持单调递增或单调递减。以单调递增栈为例,当一个新元素准备入栈时,如果它比栈顶元素小,就必须把栈顶 “踢出来”,直到新元素比栈顶大或者栈空了,它才进去。之所以这样做,是因为单调栈总能以 O(n) 的时间复杂度,找到数组中每个元素左/右边第一个比它大/小的元素。 如果用暴力循环,这需要 O(n^2)。


双端栈

双端栈(也叫双栈)是指两个栈共享同一个数组空间。

  • 物理模型:一个长数组,栈 A 从左头开始向右增长,栈 B 从右头开始向左增长。
  • 判满条件:当两个栈的栈顶指针 “相遇” 时(topA + 1 == topB),才说明数组真的满了。

如果分别给两个栈开辟数组,可能栈 A 溢出了,栈 B 却还空着大半,这叫 “贫富不均”。双端栈让两个栈互补余缺,极大地提高了内存利用率。应用场景如下:

  • 虚拟机/解释器:在某些脚本语言引擎中,一个栈用于存储数据(操作数栈),另一个用于存储函数调用(帧栈)。由于无法预知谁用得多,共享空间是最佳选择。
  • 前后端数据分离:在处理某些需要对比两端数据的算法时,用一个数组维护两个增长方向。