1. 栈的基本概念与Java实现
栈(Stack)是一种遵循后进先出(LIFO)原则的线性数据结构,就像我们日常生活中叠放的盘子,总是最后放上去的盘子最先被取用。在Java中,栈的核心操作包括:
- push(element): 将元素压入栈顶
- pop(): 移除并返回栈顶元素
- peek(): 返回栈顶元素但不移除
- isEmpty(): 判断栈是否为空
- size(): 返回栈中元素数量
1.1 基于数组的栈实现
数组实现栈的优势在于内存连续、访问速度快,适合元素数量可预估的场景。以下是完整实现:
java复制public class ArrayStack<E> {
private static final int DEFAULT_CAPACITY = 10;
private Object[] elements;
private int top; // 栈顶指针
public ArrayStack() {
this(DEFAULT_CAPACITY);
}
public ArrayStack(int initialCapacity) {
if (initialCapacity <= 0) {
throw new IllegalArgumentException("初始容量必须大于0");
}
this.elements = new Object[initialCapacity];
this.top = -1; // 初始为空栈
}
public void push(E element) {
if (top == elements.length - 1) {
resize(); // 自动扩容
}
elements[++top] = element;
}
public E pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E element = (E) elements[top];
elements[top--] = null; // 帮助GC
return element;
}
public E peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E element = (E) elements[top];
return element;
}
public boolean isEmpty() {
return top == -1;
}
public int size() {
return top + 1;
}
private void resize() {
int newCapacity = elements.length * 2;
elements = Arrays.copyOf(elements, newCapacity);
}
}
关键点说明:数组实现需要考虑动态扩容,这里采用翻倍策略。top指针初始为-1表示空栈,每次push先移动指针再赋值,pop先取值再移动指针。
1.2 基于链表的栈实现
链表实现栈的优势在于无需预先分配固定空间,适合元素数量变化大的场景:
java复制public class LinkedStack<E> {
private static class Node<E> {
E data;
Node<E> next;
Node(E data, Node<E> next) {
this.data = data;
this.next = next;
}
}
private Node<E> top;
private int size;
public void push(E element) {
top = new Node<>(element, top);
size++;
}
public E pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
E element = top.data;
top = top.next;
size--;
return element;
}
// peek(), isEmpty(), size() 实现与数组版本类似
}
链表实现中,栈顶即为链表头节点,push操作在头部插入,pop操作移除头节点。这种实现没有容量限制,但每个元素需要额外空间存储next指针。
2. 最小栈的设计与实现
最小栈(Min Stack)是一种特殊栈结构,除了常规栈操作外,还能在O(1)时间内获取栈中的最小元素。这在需要频繁查询最小值的场景非常有用,如算法题中的"柱状图中最大矩形"问题。
2.1 辅助栈法
最常见的实现方式是使用辅助栈同步存储最小值:
java复制public class MinStack {
private Deque<Integer> mainStack;
private Deque<Integer> minStack;
public MinStack() {
mainStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
mainStack.push(val);
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
} else {
minStack.push(minStack.peek()); // 重复当前最小值
}
}
public void pop() {
mainStack.pop();
minStack.pop();
}
public int top() {
return mainStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
时间复杂度分析:
- push/pop/top/getMin均为O(1)
- 空间复杂度O(n),因为需要额外存储最小值
实际开发中,可以使用Java标准库的Deque接口实现,ArrayDeque底层基于数组,性能优于Stack类。
2.2 差值存储法(空间优化版)
当栈元素较多时,可以采用差值法减少空间占用:
java复制public class MinStackOptimized {
private long min;
private Deque<Long> stack;
public MinStackOptimized() {
stack = new ArrayDeque<>();
}
public void push(int x) {
if (stack.isEmpty()) {
min = x;
stack.push(0L);
} else {
long diff = x - min;
stack.push(diff);
if (diff < 0) {
min = x; // 更新最小值
}
}
}
public void pop() {
long diff = stack.pop();
if (diff < 0) {
min = min - diff; // 恢复前一个最小值
}
}
public int top() {
long diff = stack.peek();
if (diff < 0) {
return (int)min;
} else {
return (int)(min + diff);
}
}
public int getMin() {
return (int)min;
}
}
优化原理:
- 存储当前元素与前一个最小值的差值
- 当差值为负时,表示当前元素是最小值
- pop时根据差值恢复前一个最小值
- 空间复杂度仍为O(n),但常数因子更小
注意:这种方法可能引发数值溢出问题,当差值超过long范围时会出错。实际使用中需要评估元素范围。
3. 栈的应用场景与实战案例
3.1 括号匹配校验
栈非常适合处理成对出现的符号匹配问题:
java复制public boolean isValidParentheses(String s) {
Deque<Character> stack = new ArrayDeque<>();
Map<Character, Character> pairs = Map.of(
')', '(',
']', '[',
'}', '{'
);
for (char c : s.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.2 浏览器前进后退功能
浏览器历史记录正是栈的典型应用:
java复制public class BrowserHistory {
private Deque<String> backStack = new ArrayDeque<>();
private Deque<String> forwardStack = new ArrayDeque<>();
private String current;
public BrowserHistory(String homepage) {
current = homepage;
}
public void visit(String url) {
backStack.push(current);
current = url;
forwardStack.clear(); // 新访问时清空前进栈
}
public String back(int steps) {
while (steps-- > 0 && !backStack.isEmpty()) {
forwardStack.push(current);
current = backStack.pop();
}
return current;
}
public String forward(int steps) {
while (steps-- > 0 && !forwardStack.isEmpty()) {
backStack.push(current);
current = forwardStack.pop();
}
return current;
}
}
3.3 逆波兰表达式求值
栈可以高效计算后缀表达式:
java复制public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
if (token.length() == 1 && "+-*/".contains(token)) {
int b = stack.pop();
int a = stack.pop();
switch (token) {
case "+": stack.push(a + b); break;
case "-": stack.push(a - b); break;
case "*": stack.push(a * b); break;
case "/": stack.push(a / b); break;
}
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
4. 栈的常见问题与优化策略
4.1 线程安全考虑
Java中的Stack类是线程安全的,但性能较差。实际开发中更推荐:
- 使用Collections.synchronizedCollection包装:
java复制Deque<Integer> stack = Collections.synchronizedCollection(new ArrayDeque<>());
- 使用ConcurrentLinkedDeque(适合高并发):
java复制Deque<Integer> stack = new ConcurrentLinkedDeque<>();
- 对于读多写少场景,可以考虑CopyOnWriteArrayList
4.2 内存分配优化
对于性能敏感场景,可以优化数组栈的扩容策略:
- 预分配足够容量:根据业务场景预估最大容量
- 增量式扩容:改为每次增加固定大小而非翻倍
- 对象池技术:复用弹出的栈元素对象
java复制public class OptimizedArrayStack<E> {
private static final int MAX_CAPACITY = 1024;
private Object[] elements;
private int top;
private final ReusableObjectPool<E> pool; // 假设有对象池实现
private void resize() {
int newCapacity = elements.length + (elements.length >> 1); // 1.5倍
if (newCapacity > MAX_CAPACITY) {
newCapacity = MAX_CAPACITY;
if (top == MAX_CAPACITY - 1) {
throw new StackOverflowError();
}
}
elements = Arrays.copyOf(elements, newCapacity);
}
public E pop() {
// ... 原有逻辑
pool.returnObject(element); // 归还对象到池中
return element;
}
}
4.3 栈溢出防护
递归调用可能引发栈溢出,解决方案:
- 限制递归深度:
java复制public void recursiveMethod(int depth) {
if (depth > 1000) {
throw new StackOverflowError("递归深度超过限制");
}
// ... 递归逻辑
}
- 将递归改为迭代+显式栈:
java复制public void traverseTree(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
// 处理节点
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
}
4.4 最小栈的变体实现
根据业务需求,最小栈可以有多种变体:
- 最大栈:同理实现getMax()功能
- 中间值栈:维护栈的中位数(需要更复杂的数据结构)
- 频率栈:记录元素出现频率
java复制// 频率栈实现示例
public class FreqStack {
private Deque<Integer> stack;
private Map<Integer, Integer> freqMap;
private Map<Integer, Deque<Integer>> groupMap;
private int maxFreq;
public FreqStack() {
stack = new ArrayDeque<>();
freqMap = new HashMap<>();
groupMap = new HashMap<>();
}
public void push(int val) {
int freq = freqMap.getOrDefault(val, 0) + 1;
freqMap.put(val, freq);
if (freq > maxFreq) {
maxFreq = freq;
}
groupMap.computeIfAbsent(freq, k -> new ArrayDeque<>()).push(val);
}
public int pop() {
int val = groupMap.get(maxFreq).pop();
freqMap.put(val, freqMap.get(val) - 1);
if (groupMap.get(maxFreq).isEmpty()) {
maxFreq--;
}
return val;
}
}
