1. 栈数据结构基础认知
栈(Stack)是计算机科学中最基础且重要的线性数据结构之一,其核心特性可概括为"后进先出"(LIFO:Last In First Out)。这种特性使得栈在程序执行、算法实现和系统设计中具有不可替代的作用。
1.1 栈的核心特性
栈的操作限制在表的一端进行,这一端被称为栈顶(top),相对的另一端则称为栈底(bottom)。这种受限的访问方式带来了几个关键特征:
- 数据元素的插入(push)和删除(pop)只能在栈顶进行
- 最后入栈的元素总是最先被访问
- 栈中元素的访问具有单向性
这种特性与日常生活中的弹簧托盘或书堆非常相似——我们总是从最上面取放物品,而最早放入的物品会被压在底部。
1.2 栈的ADT定义
作为抽象数据类型(ADT),栈的基本操作包括:
public interface Stack<E> { void push(E item); // 入栈操作 E pop(); // 出栈操作 E peek(); // 查看栈顶元素 boolean isEmpty(); // 判空 int size(); // 获取大小 }在Java集合框架中,虽然Stack类存在,但由于设计问题(继承自Vector),实际开发中更推荐使用Deque接口的实现类如ArrayDeque来模拟栈操作。
2. Java中的栈实现剖析
2.1 基于数组的实现
数组实现栈是最直观的方式之一,其核心在于维护一个top指针:
public class ArrayStack<E> { private E[] elements; private int top = -1; private static final int DEFAULT_CAPACITY = 16; public ArrayStack() { this(DEFAULT_CAPACITY); } public ArrayStack(int capacity) { elements = (E[]) new Object[capacity]; } public void push(E item) { if (top == elements.length - 1) { resize(elements.length * 2); } elements[++top] = item; } public E pop() { if (isEmpty()) throw new EmptyStackException(); E item = elements[top]; elements[top--] = null; // 避免内存泄漏 if (top > 0 && top == elements.length / 4) { resize(elements.length / 2); } return item; } private void resize(int newSize) { E[] newArray = (E[]) new Object[newSize]; System.arraycopy(elements, 0, newArray, 0, top + 1); elements = newArray; } }这种实现方式的优缺点:
- 优点:内存连续,访问速度快;实现简单直观
- 缺点:需要预先分配空间,扩容/缩容有性能开销
2.2 基于链表的实现
链表实现栈更加灵活,不需要处理扩容问题:
public class LinkedStack<E> { private static class Node<E> { E item; Node<E> next; Node(E item, Node<E> next) { this.item = item; this.next = next; } } private Node<E> top; private int size; public void push(E item) { top = new Node<>(item, top); size++; } public E pop() { if (isEmpty()) throw new EmptyStackException(); E item = top.item; top = top.next; size--; return item; } }链表实现的特性:
- 优点:动态扩容,没有空间浪费
- 缺点:每个元素需要额外空间存储指针;内存不连续可能影响缓存命中率
2.3 Java标准库中的栈
Java集合框架提供了Stack类,但存在设计缺陷:
// 不推荐的使用方式 Stack<String> stack = new Stack<>(); stack.push("item"); String s = stack.pop();更现代的替代方案是使用Deque接口:
// 推荐的使用方式 Deque<String> stack = new ArrayDeque<>(); stack.push("item"); String s = stack.pop();重要提示:Java的Stack类由于继承自Vector,所有方法都是同步的,这在单线程环境下会造成不必要的性能开销。这也是为什么官方文档建议使用Deque实现来代替Stack类。
3. 栈的核心应用场景
3.1 函数调用栈
JVM使用调用栈(Call Stack)来管理方法调用和返回:
- 每个方法调用会创建一个栈帧(Stack Frame)压入栈中
- 栈帧包含局部变量表、操作数栈、动态链接和方法返回地址
- 方法执行完毕时对应的栈帧出栈
示例代码的执行过程:
public class CallStackDemo { public static void main(String[] args) { int a = 10; int b = 20; int result = add(a, b); System.out.println(result); } static int add(int x, int y) { int sum = x + y; return sum; } }对应的栈状态变化:
- main()方法栈帧入栈
- add()方法栈帧入栈(此时栈中有两个栈帧)
- add()执行完毕出栈
- main()执行完毕出栈
3.2 表达式求值
栈在表达式求值中有两种典型应用:
- 中缀表达式转后缀表达式
- 后缀表达式求值
中缀转后缀算法(调度场算法):
public static String infixToPostfix(String infix) { StringBuilder output = new StringBuilder(); Deque<Character> stack = new ArrayDeque<>(); for (char c : infix.toCharArray()) { if (Character.isDigit(c)) { output.append(c); } else if (c == '(') { stack.push(c); } else if (c == ')') { while (!stack.isEmpty() && stack.peek() != '(') { output.append(stack.pop()); } stack.pop(); // 弹出左括号 } else { while (!stack.isEmpty() && precedence(c) <= precedence(stack.peek())) { output.append(stack.pop()); } stack.push(c); } } while (!stack.isEmpty()) { output.append(stack.pop()); } return output.toString(); } private static int precedence(char op) { switch (op) { case '+': case '-': return 1; case '*': case '/': return 2; default: return 0; } }后缀表达式求值算法:
public static int evaluatePostfix(String postfix) { Deque<Integer> stack = new ArrayDeque<>(); for (char c : postfix.toCharArray()) { if (Character.isDigit(c)) { stack.push(c - '0'); } else { int b = stack.pop(); int a = stack.pop(); switch (c) { case '+': stack.push(a + b); break; case '-': stack.push(a - b); break; case '*': stack.push(a * b); break; case '/': stack.push(a / b); break; } } } return stack.pop(); }3.3 括号匹配检查
栈非常适合解决成对出现的符号匹配问题:
public static boolean isBalanced(String expression) { Deque<Character> stack = new ArrayDeque<>(); Map<Character, Character> pairs = Map.of(')', '(', ']', '[', '}', '{'); for (char c : expression.toCharArray()) { if (pairs.containsValue(c)) { stack.push(c); } else if (pairs.containsKey(c)) { if (stack.isEmpty() || stack.pop() != pairs.get(c)) { return false; } } } return stack.isEmpty(); }3.4 浏览器历史记录
浏览器的前进后退功能就是典型的栈应用:
- 访问新页面时URL压入后退栈
- 点击后退时从后退栈弹出并压入前进栈
- 点击前进时从前进栈弹出并压入后退栈
简化实现:
public class BrowserHistory { private Deque<String> backStack = new ArrayDeque<>(); private Deque<String> forwardStack = new ArrayDeque<>(); private String current; public void visit(String url) { if (current != null) { backStack.push(current); } current = url; forwardStack.clear(); } public String back() { if (backStack.isEmpty()) return null; forwardStack.push(current); current = backStack.pop(); return current; } public String forward() { if (forwardStack.isEmpty()) return null; backStack.push(current); current = forwardStack.pop(); return current; } }4. 栈的高级应用与优化
4.1 单调栈及其应用
单调栈是指栈内元素保持单调递增或递减的特殊栈结构,常用于解决"下一个更大元素"类问题。
示例:每日温度问题(找出后面第一个更高温度的天数)
public int[] dailyTemperatures(int[] temperatures) { int[] result = new int[temperatures.length]; Deque<Integer> stack = new ArrayDeque<>(); // 存储索引 for (int i = 0; i < temperatures.length; i++) { while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) { int prev = stack.pop(); result[prev] = i - prev; } stack.push(i); } return result; }4.2 栈与递归的转换
递归本质上就是使用系统调用栈,任何递归算法都可以用栈转换为迭代实现。
以二叉树中序遍历为例: 递归版本:
public void inorderTraversal(TreeNode root) { if (root == null) return; inorderTraversal(root.left); System.out.println(root.val); inorderTraversal(root.right); }迭代版本(使用栈):
public void inorderTraversal(TreeNode root) { Deque<TreeNode> stack = new ArrayDeque<>(); TreeNode current = root; while (current != null || !stack.isEmpty()) { while (current != null) { stack.push(current); current = current.left; } current = stack.pop(); System.out.println(current.val); current = current.right; } }4.3 多栈共享空间
在某些内存受限的场景,可以采用一个数组实现多个栈:
public class MultiStack { private int[] values; private int[] tops; private int[] next; private int free; private int stackCount; private int capacity; public MultiStack(int stackCount, int totalCapacity) { this.stackCount = stackCount; this.capacity = totalCapacity; values = new int[totalCapacity]; tops = new int[stackCount]; Arrays.fill(tops, -1); next = new int[totalCapacity]; for (int i = 0; i < totalCapacity - 1; i++) { next[i] = i + 1; } next[totalCapacity - 1] = -1; free = 0; } public void push(int stackNum, int value) { if (isFull()) throw new IllegalStateException("Stack overflow"); int index = free; free = next[index]; next[index] = tops[stackNum]; tops[stackNum] = index; values[index] = value; } public int pop(int stackNum) { if (isEmpty(stackNum)) throw new EmptyStackException(); int index = tops[stackNum]; tops[stackNum] = next[index]; next[index] = free; free = index; return values[index]; } }4.4 栈的最小值优化
如何在O(1)时间内获取栈的最小值?以下是两种实现方式:
辅助栈法:
public class MinStack { private Deque<Integer> stack = new ArrayDeque<>(); private Deque<Integer> minStack = new ArrayDeque<>(); public void push(int x) { stack.push(x); if (minStack.isEmpty() || x <= minStack.peek()) { minStack.push(x); } } public int pop() { int x = stack.pop(); if (x == minStack.peek()) { minStack.pop(); } return x; } public int getMin() { return minStack.peek(); } }差值存储法(节省空间):
public class MinStack { private Deque<Long> stack = new ArrayDeque<>(); private long min; public void push(int x) { if (stack.isEmpty()) { stack.push(0L); min = x; } else { stack.push(x - min); if (x < min) min = x; } } public int pop() { long diff = stack.pop(); if (diff < 0) { int res = (int)min; min = min - diff; return res; } return (int)(min + diff); } public int getMin() { return (int)min; } }5. 栈相关的常见问题与解决方案
5.1 栈溢出问题
栈溢出(StackOverflowError)通常发生在递归深度过大时。解决方法包括:
- 增加栈大小(JVM参数-Xss)
- 将递归改为迭代
- 使用尾递归优化(Java暂不支持)
示例:递归导致的栈溢出
public static void infiniteRecursion() { infiniteRecursion(); // 无限递归调用 }5.2 线程安全考虑
在多线程环境下使用栈需要注意:
- Java的Stack类是线程安全的(继承自Vector)
- ArrayDeque不是线程安全的
- 解决方案:
// 使用Collections工具类包装 Deque<String> safeStack = Collections.synchronizedDeque(new ArrayDeque<>()); // 或者使用ConcurrentLinkedDeque Deque<String> concurrentStack = new ConcurrentLinkedDeque<>();
5.3 内存分配问题
栈实现需要考虑内存分配策略:
- 固定大小栈:简单但可能溢出或浪费空间
- 动态扩容栈:更灵活但有复制开销
- 链式栈:无扩容问题但内存不连续
性能对比测试示例:
public class StackBenchmark { @Benchmark public void arrayStack(Blackhole bh) { ArrayStack<Integer> stack = new ArrayStack<>(); for (int i = 0; i < 100000; i++) { stack.push(i); } while (!stack.isEmpty()) { bh.consume(stack.pop()); } } @Benchmark public void linkedStack(Blackhole bh) { LinkedStack<Integer> stack = new LinkedStack<>(); for (int i = 0; i < 100000; i++) { stack.push(i); } while (!stack.isEmpty()) { bh.consume(stack.pop()); } } }5.4 栈的序列问题
经典栈问题:判断给定序列是否是合法的栈弹出序列
public boolean isPopSequence(int[] pushed, int[] popped) { Deque<Integer> stack = new ArrayDeque<>(); int popIndex = 0; for (int num : pushed) { stack.push(num); while (!stack.isEmpty() && stack.peek() == popped[popIndex]) { stack.pop(); popIndex++; } } return popIndex == popped.length; }5.5 表达式解析中的边界情况
处理复杂表达式时需要考虑多种边界情况:
- 负数处理
- 多位数字识别
- 空格等空白字符
- 非法字符检测
增强版表达式求值:
public static int evaluateExpression(String expression) { Deque<Integer> operands = new ArrayDeque<>(); Deque<Character> operators = new ArrayDeque<>(); for (int i = 0; i < expression.length(); i++) { char c = expression.charAt(i); if (c == ' ') continue; if (Character.isDigit(c)) { int num = 0; while (i < expression.length() && Character.isDigit(expression.charAt(i))) { num = num * 10 + (expression.charAt(i++) - '0'); } i--; operands.push(num); } else if (c == '(') { operators.push(c); } else if (c == ')') { while (operators.peek() != '(') { operands.push(applyOp(operators.pop(), operands.pop(), operands.pop())); } operators.pop(); } else if (isOperator(c)) { while (!operators.isEmpty() && precedence(c) <= precedence(operators.peek())) { operands.push(applyOp(operators.pop(), operands.pop(), operands.pop())); } operators.push(c); } } while (!operators.isEmpty()) { operands.push(applyOp(operators.pop(), operands.pop(), operands.pop())); } return operands.pop(); } private static boolean isOperator(char c) { return c == '+' || c == '-' || c == '*' || c == '/'; } private static int applyOp(char op, int b, int a) { switch (op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b; default: throw new IllegalArgumentException("Unknown operator: " + op); } }