1. 栈的基本概念与Java实现
栈(Stack)是一种遵循"后进先出"(LIFO)原则的线性数据结构,就像我们日常生活中叠放的盘子,总是先取用最上面那个最后放进去的盘子。在Java中,栈的基本操作包括:
- push(element): 将元素压入栈顶
- pop(): 移除并返回栈顶元素
- peek(): 返回栈顶元素但不移除
- isEmpty(): 判断栈是否为空
- size(): 返回栈中元素数量
1.1 基于数组的栈实现
java复制public class ArrayStack<E> {
private Object[] elements;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public ArrayStack() {
elements = new Object[DEFAULT_CAPACITY];
}
public void push(E element) {
if(size == elements.length) {
ensureCapacity();
}
elements[size++] = element;
}
public E pop() {
if(isEmpty()) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E element = (E)elements[--size];
elements[size] = null; // 帮助GC
return element;
}
public E peek() {
if(isEmpty()) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E element = (E)elements[size-1];
return element;
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
private void ensureCapacity() {
int newCapacity = elements.length * 2;
elements = Arrays.copyOf(elements, newCapacity);
}
}
注意:数组实现需要考虑扩容问题,当栈满时需要动态扩容,这里采用了常见的翻倍扩容策略。
1.2 基于链表的栈实现
java复制public class LinkedStack<E> {
private static class Node<E> {
E item;
Node<E> next;
Node(E element, Node<E> next) {
this.item = element;
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.item;
top = top.next;
size--;
return element;
}
public E peek() {
if(isEmpty()) {
throw new EmptyStackException();
}
return top.item;
}
public boolean isEmpty() {
return top == null;
}
public int size() {
return size;
}
}
链表实现的优势在于不需要考虑容量问题,每次push操作都创建一个新节点,内存使用更加灵活。
2. 最小栈的设计与实现
最小栈(Min Stack)是一种特殊的栈数据结构,除了支持常规的栈操作外,还能在O(1)时间内获取栈中的最小元素。这在某些算法问题中非常有用,比如需要频繁获取当前最小值的场景。
2.1 辅助栈法实现最小栈
最常见的实现方式是使用辅助栈来同步存储最小值:
java复制public class MinStack {
private Deque<Integer> stack;
private Deque<Integer> minStack;
public MinStack() {
stack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
stack.push(val);
if(minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
int val = stack.pop();
if(val == minStack.peek()) {
minStack.pop();
}
}
public int top() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
return stack.peek();
}
public int getMin() {
if(minStack.isEmpty()) {
throw new EmptyStackException();
}
return minStack.peek();
}
}
这种实现方式的空间复杂度是O(n),因为最坏情况下需要存储所有元素的最小值。
2.2 差值法实现最小栈(优化空间)
对于内存敏感的场景,可以使用差值法来优化空间:
java复制public class MinStackOptimized {
private Deque<Long> stack;
private long min;
public MinStackOptimized() {
stack = new ArrayDeque<>();
}
public void push(int val) {
if(stack.isEmpty()) {
stack.push(0L);
min = val;
} else {
stack.push((long)val - min);
if(val < min) {
min = val;
}
}
}
public void pop() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
long diff = stack.pop();
if(diff < 0) {
min = min - diff; // 恢复前一个min
}
}
public int top() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
long diff = stack.peek();
if(diff > 0) {
return (int)(min + diff);
} else {
return (int)min;
}
}
public int getMin() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
return (int)min;
}
}
差值法通过存储当前值与最小值的差值来优化空间,空间复杂度仍然是O(n),但实际存储的数据量更小,特别适合存储大对象的情况。
3. 栈的应用场景与实战案例
3.1 括号匹配检查
栈非常适合解决括号匹配问题,算法思路如下:
- 初始化一个空栈
- 遍历字符串中的每个字符
- 遇到左括号就压栈
- 遇到右括号就检查栈顶是否匹配
- 最后检查栈是否为空
java复制public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for(char c : s.toCharArray()) {
if(c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if(stack.isEmpty()) return false;
char top = stack.pop();
if(!((top == '(' && c == ')') ||
(top == '[' && c == ']') ||
(top == '{' && c == '}'))) {
return false;
}
}
}
return stack.isEmpty();
}
3.2 表达式求值
栈可以用于计算算术表达式,特别是包含括号和运算符优先级的复杂表达式:
java复制public int calculate(String s) {
Deque<Integer> nums = new ArrayDeque<>();
Deque<Character> ops = new ArrayDeque<>();
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(Character.isDigit(c)) {
int num = 0;
while(i < s.length() && Character.isDigit(s.charAt(i))) {
num = num * 10 + (s.charAt(i) - '0');
i++;
}
i--;
nums.push(num);
} else if(c == '(') {
ops.push(c);
} else if(c == ')') {
while(ops.peek() != '(') {
nums.push(applyOp(ops.pop(), nums.pop(), nums.pop()));
}
ops.pop(); // 弹出'('
} else if(c == '+' || c == '-' || c == '*' || c == '/') {
while(!ops.isEmpty() && hasPrecedence(c, ops.peek())) {
nums.push(applyOp(ops.pop(), nums.pop(), nums.pop()));
}
ops.push(c);
}
}
while(!ops.isEmpty()) {
nums.push(applyOp(ops.pop(), nums.pop(), nums.pop()));
}
return nums.pop();
}
private boolean hasPrecedence(char op1, char op2) {
if(op2 == '(' || op2 == ')') return false;
if((op1 == '*' || op1 == '/') && (op2 == '+' || op2 == '-')) return false;
return true;
}
private 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;
}
return 0;
}
3.3 浏览器前进后退功能
浏览器的前进后退功能可以通过两个栈来实现:
- 一个栈存储后退页面
- 另一个栈存储前进页面
java复制public class BrowserHistory {
private Deque<String> backStack;
private Deque<String> forwardStack;
private String current;
public BrowserHistory(String homepage) {
backStack = new ArrayDeque<>();
forwardStack = new ArrayDeque<>();
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;
}
public String getCurrent() {
return current;
}
}
4. 常见问题与性能优化
4.1 线程安全问题
标准栈实现不是线程安全的,在多线程环境下需要使用同步机制:
java复制public class SynchronizedStack<E> {
private final Deque<E> stack = new ArrayDeque<>();
public synchronized void push(E element) {
stack.push(element);
}
public synchronized E pop() {
return stack.pop();
}
public synchronized E peek() {
return stack.peek();
}
public synchronized boolean isEmpty() {
return stack.isEmpty();
}
}
或者使用Java并发包中的线程安全实现:
java复制Deque<E> stack = new ConcurrentLinkedDeque<>();
4.2 内存泄漏问题
在使用对象栈时,pop操作后如果不手动置空引用,可能会导致内存泄漏:
java复制public E pop() {
if(isEmpty()) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E element = (E)elements[--size];
elements[size] = null; // 清除引用,防止内存泄漏
return element;
}
4.3 栈溢出问题
递归调用本质上就是使用系统栈,深度递归可能导致栈溢出:
java复制// 不安全的递归实现
public int factorial(int n) {
if(n == 1) return 1;
return n * factorial(n - 1); // 深度过大会栈溢出
}
// 使用栈结构实现的迭代版本
public int factorial(int n) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(n);
int result = 1;
while(!stack.isEmpty()) {
int num = stack.pop();
result *= num;
if(num > 1) {
stack.push(num - 1);
}
}
return result;
}
4.4 最小栈的优化变种
对于特定场景,可以进一步优化最小栈的实现。例如,当栈中元素范围有限时,可以使用计数法:
java复制public class MinStackWithCount {
private Deque<Integer> stack;
private Deque<int[]> minStack; // 存储[最小值, 出现次数]
public MinStackWithCount() {
stack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
stack.push(val);
if(minStack.isEmpty() || val < minStack.peek()[0]) {
minStack.push(new int[]{val, 1});
} else if(val == minStack.peek()[0]) {
minStack.peek()[1]++;
}
}
public void pop() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
int val = stack.pop();
if(val == minStack.peek()[0]) {
if(--minStack.peek()[1] == 0) {
minStack.pop();
}
}
}
public int top() {
if(stack.isEmpty()) {
throw new EmptyStackException();
}
return stack.peek();
}
public int getMin() {
if(minStack.isEmpty()) {
throw new EmptyStackException();
}
return minStack.peek()[0];
}
}
这种实现减少了辅助栈的操作次数,在最小值频繁重复出现的场景下性能更好。
