Java 栈 - 附LeetCode 经典题解
从底层原理到实战应用,彻底掌握 Java 栈的使用
一、栈的基础概念
1.1 什么是栈
想象一摞盘子:只能在最上面放盘子(入栈 push),只能拿最上面的盘子(出栈 pop),只能看最上面的盘子(查看栈顶 peek)。这就是栈的核心特性:后进先出(LIFO - Last In First Out)。
tex
栈的操作示意:
┌───┐
│ 3 │ ← 栈顶(最后进来的)
├───┤
│ 2 │
├───┤
│ 1 │ ← 栈底(最先进来的)
└───┘
栈是一种线性数据结构,只允许在一端(栈顶)进行插入和删除操作。核心特性包括:
- LIFO:后进先出
- 单端操作:只能在栈顶操作
- 受限访问:不能随机访问中间元素
1.2 栈的应用场景
栈在计算机科学中应用广泛:
- 函数调用栈:方法调用和返回
- 表达式求值:中缀转后缀、计算器实现
- 括号匹配:检查括号是否配对
- 浏览器历史:前进/后退功能
- 撤销操作:Ctrl+Z 功能实现
- 深度优先搜索(DFS):图和树的遍历
- 递归转迭代:用栈模拟递归调用
二、Java 栈的实现方式对比
Java 提供了多种实现栈的方式,每种都有优缺点。
2.1 实现方式总览
| 实现方式 | 底层结构 | 线程安全 | 性能 | 推荐度 | 备注 |
|---|---|---|---|---|---|
| Stack | 数组(继承 Vector) | 是 | 慢 | 不推荐 | 已过时,官方不推荐 |
| Deque (接口) | - | - | - | 推荐 | 官方推荐的栈接口 |
| ArrayDeque | 循环数组 | 否 | 最快 | 强烈推荐 | 单线程最佳选择 |
| LinkedList | 双向链表 | 否 | 中等 | 推荐 | 适合频繁插入删除 |
| ConcurrentLinkedDeque | 双向链表 | 是 | 中等 | 多线程推荐 | 线程安全 |
官方推荐:使用 Deque 接口 + ArrayDeque 实现。
2.2 为什么不推荐 Stack 类
Stack 类是 Java 1.0 就有的老古董,存在以下问题:
问题 1:继承了 Vector,性能差
java
public class Stack<E> extends Vector<E> {
// Stack 继承了 Vector,所有方法都是 synchronized 的
}
Vector 的所有方法都加了 synchronized 锁,即使在单线程环境下也会有锁的开销,性能比 ArrayDeque 慢很多。
问题 2:破坏了栈的封装性
java
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
// 问题:可以随机访问和修改元素,破坏了栈的 LIFO 特性
stack.get(0); // 可以访问栈底元素
stack.set(1, 999); // 可以修改中间元素
stack.add(0, 888); // 可以在任意位置插入元素
这些操作违背了栈的设计原则。
问题 3:官方已不推荐
Java 官方文档明确指出:
A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.
翻译:Deque 接口及其实现提供了更完整和一致的 LIFO 栈操作,应优先使用它而不是 Stack 类。
2.3 推荐方案:Deque + ArrayDeque
2.3.1 什么是 Deque
**Deque(Double Ended Queue,双端队列)**是 Java 6 引入的接口,支持在两端进行操作。
java
public interface Deque<E> extends Queue<E> {
// 栈操作
void push(E e);
E pop();
E peek();
// 队列操作
boolean offer(E e);
E poll();
// 双端队列操作
void addFirst(E e);
void addLast(E e);
E removeFirst();
E removeLast();
}
为什么用 Deque 当栈:
- 提供了完整的栈操作方法(push、pop、peek)
- 不破坏封装性(没有随机访问方法)
- 有多种高性能实现(ArrayDeque、LinkedList)
2.3.2 ArrayDeque:最佳选择
java
import java.util.Deque;
import java.util.ArrayDeque;
// 推荐写法
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.pop()); // 3
ArrayDeque 的优势:
- 性能最好:基于循环数组,无锁开销
- 内存效率高:没有节点对象的额外开销
- 扩容机制:自动扩容,初始容量 16
- 不允许 null:避免空指针问题
底层原理:
java
// ArrayDeque 内部结构
transient Object[] elements; // 循环数组
transient int head; // 头指针
transient int tail; // 尾指针
// 栈操作映射到数组操作
push(e) → elements[--head] = e; // 在头部插入
pop() → elements[head++]; // 从头部删除
2.3.3 LinkedList:备选方案
java
import java.util.Deque;
import java.util.LinkedList;
// 也可以用 LinkedList
Deque<Integer> stack = new LinkedList<>();
stack.push(1);
stack.push(2);
LinkedList 的特点:
- 基于双向链表:每个元素是一个 Node 对象
- 适合频繁插入删除:不需要移动元素
- 允许 null:可以存储 null 值
- 内存开销大:每个节点需要额外的 prev 和 next 指针
底层原理:
java
// LinkedList 内部结构
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点
// 栈操作映射到链表操作
push(e) → addFirst(e); // 在头部插入
pop() → removeFirst(); // 从头部删除
2.4 性能对比实测
java
public class StackPerformanceTest {
public static void main(String[] args) {
int n = 1000000;
// 测试 Stack
long start = System.currentTimeMillis();
Stack<Integer> stack1 = new Stack<>();
for (int i = 0; i < n; i++) {
stack1.push(i);
}
for (int i = 0; i < n; i++) {
stack1.pop();
}
System.out.println("Stack: " + (System.currentTimeMillis() - start) + "ms");
// 测试 ArrayDeque
start = System.currentTimeMillis();
Deque<Integer> stack2 = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
stack2.push(i);
}
for (int i = 0; i < n; i++) {
stack2.pop();
}
System.out.println("ArrayDeque: " + (System.currentTimeMillis() - start) + "ms");
// 测试 LinkedList
start = System.currentTimeMillis();
Deque<Integer> stack3 = new LinkedList<>();
for (int i = 0; i < n; i++) {
stack3.push(i);
}
for (int i = 0; i < n; i++) {
stack3.pop();
}
System.out.println("LinkedList: " + (System.currentTimeMillis() - start) + "ms");
}
}
测试结果(100 万次操作):
Stack: ~150ms (慢)
ArrayDeque: ~50ms (最快)
LinkedList: ~80ms (中等)
结论:ArrayDeque 性能最好,是栈的首选实现。
三、栈的方法详解
3.1 核心方法
3.1.1 push() - 入栈
java
Deque<Integer> stack = new ArrayDeque<>();
// 将元素压入栈顶
stack.push(1);
stack.push(2);
stack.push(3);
// 栈的状态:[3, 2, 1](3 在栈顶)
方法签名:
java
void push(E e);
特点:
- 在栈顶插入元素
- ArrayDeque 会自动扩容,不会抛异常
- 不允许插入 null(ArrayDeque)
等价方法:
java
stack.push(1); // 推荐
stack.addFirst(1); // 等价,但语义不如 push 清晰
3.1.2 pop() - 出栈
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
int top = stack.pop(); // 返回 3,栈变为 [2, 1]
方法签名:
java
E pop();
特点:
- 移除并返回栈顶元素
- 如果栈为空,抛出 NoSuchElementException
- 时间复杂度 O(1)
等价方法:
java
stack.pop(); // 推荐
stack.removeFirst(); // 等价
安全版本:
java
// 如果不确定栈是否为空,先判断
if (!stack.isEmpty()) {
int top = stack.pop();
}
// 或者使用 poll()(不抛异常)
Integer top = stack.poll(); // 栈为空时返回 null
3.1.3 peek() - 查看栈顶
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
int top = stack.peek(); // 返回 3,栈不变,仍为 [3, 2, 1]
方法签名:
java
E peek();
特点:
- 返回栈顶元素,但不移除
- 如果栈为空,返回 null(不抛异常)
- 时间复杂度 O(1)
等价方法:
java
stack.peek(); // 推荐
stack.peekFirst(); // 等价
stack.getFirst(); // 等价,但栈为空时抛异常
3.1.4 isEmpty() - 判断栈是否为空
java
Deque<Integer> stack = new ArrayDeque<>();
System.out.println(stack.isEmpty()); // true
stack.push(1);
System.out.println(stack.isEmpty()); // false
方法签名:
java
boolean isEmpty();
使用场景:
java
// 遍历栈(会清空栈)
while (!stack.isEmpty()) {
int top = stack.pop();
System.out.println(top);
}
3.1.5 size() - 获取栈的大小
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.size()); // 3
方法签名:
java
int size();
时间复杂度:O(1)
3.2 常用方法
3.2.1 poll() - 安全出栈
java
Deque<Integer> stack = new ArrayDeque<>();
// pop() 在栈为空时抛异常
// stack.pop(); // 抛出 NoSuchElementException
// poll() 在栈为空时返回 null
Integer top = stack.poll(); // 返回 null,不抛异常
方法签名:
java
E poll();
使用场景:
java
// 不确定栈是否为空时,用 poll() 更安全
Integer top = stack.poll();
if (top != null) {
System.out.println("栈顶元素:" + top);
} else {
System.out.println("栈为空");
}
3.2.2 clear() - 清空栈
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.clear(); // 清空栈
System.out.println(stack.isEmpty()); // true
方法签名:
java
void clear();
时间复杂度:O(n)
3.2.3 contains() - 判断元素是否在栈中
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.contains(2)); // true
System.out.println(stack.contains(5)); // false
方法签名:
java
boolean contains(Object o);
时间复杂度:O(n)(需要遍历整个栈)
注意:这个方法破坏了栈的封装性,一般不推荐使用。
3.2.4 toArray() - 转换为数组
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
// 转换为 Object 数组
Object[] arr1 = stack.toArray();
System.out.println(Arrays.toString(arr1)); // [3, 2, 1]
// 转换为指定类型数组
Integer[] arr2 = stack.toArray(new Integer[0]);
System.out.println(Arrays.toString(arr2)); // [3, 2, 1]
方法签名:
java
Object[] toArray();
<T> T[] toArray(T[] a);
3.3 不常用方法
3.3.1 iterator() - 获取迭代器
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
// 从栈顶到栈底遍历
Iterator<Integer> it = stack.iterator();
while (it.hasNext()) {
System.out.println(it.next()); // 输出:3, 2, 1
}
注意:迭代器遍历不会移除元素,栈保持不变。
3.3.2 descendingIterator() - 反向迭代器
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
// 从栈底到栈顶遍历
Iterator<Integer> it = stack.descendingIterator();
while (it.hasNext()) {
System.out.println(it.next()); // 输出:1, 2, 3
}
3.3.3 remove() - 删除指定元素
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.remove(2); // 删除元素 2
System.out.println(stack); // [3, 1]
方法签名:
java
boolean remove(Object o);
时间复杂度:O(n)
注意:这个方法破坏了栈的 LIFO 特性,不推荐使用。
3.4 Stack 类的特有方法
虽然不推荐使用 Stack 类,但了解它的方法有助于理解历史代码。
java
Stack<Integer> stack = new Stack<>();
// 1. push() - 入栈
stack.push(1);
// 2. pop() - 出栈
int top = stack.pop();
// 3. peek() - 查看栈顶
int top2 = stack.peek();
// 4. empty() - 判断是否为空(注意:不是 isEmpty())
boolean isEmpty = stack.empty();
// 5. search() - 搜索元素位置(从栈顶开始,1-based)
int pos = stack.search(1); // 返回元素距离栈顶的位置,找不到返回 -1
search() 方法示例:
java
Stack<Integer> stack = new Stack<>();
stack.push(1); // 栈底
stack.push(2);
stack.push(3); // 栈顶
System.out.println(stack.search(3)); // 1(栈顶,距离为 1)
System.out.println(stack.search(2)); // 2(距离栈顶为 2)
System.out.println(stack.search(1)); // 3(栈底,距离栈顶为 3)
System.out.println(stack.search(5)); // -1(不存在)
3.5 方法对比总结
Deque 栈操作方法对比
| 操作 | 抛异常版本 | 返回特殊值版本 | 推荐 | 说明 |
|---|---|---|---|---|
| 入栈 | push(e) | offerFirst(e) | push | push 语义更清晰 |
| 出栈 | pop() | poll() / pollFirst() | pop | 确定非空用 pop |
| 查看栈顶 | getFirst() | peek() / peekFirst() | peek | peek 不抛异常 |
| 判断空 | - | isEmpty() | isEmpty | - |
| 获取大小 | - | size() | size | - |
Stack vs Deque 方法映射
| Stack 方法 | Deque 等价方法 | 说明 |
|---|---|---|
| push(e) | push(e) | 完全相同 |
| pop() | pop() | 完全相同 |
| peek() | peek() | 完全相同 |
| empty() | isEmpty()**** | 注意方法名不同 |
| search(o) | 无直接等价 | Deque 不提供此方法 |
四、LeetCode 经典栈题目
4.1 有效的括号(LeetCode 20)
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s,判断字符串是否有效。
示例:
输入:s = "()[]{}"
输出:true
输入:s = "([)]"
输出:false
解题思路
典型的栈应用:
- 遇到左括号,入栈
- 遇到右括号,检查栈顶是否匹配
- 最后检查栈是否为空
完整代码
java
import java.util.Deque;
import java.util.ArrayDeque;
public class Solution {
public boolean isValid(String s) {
// 1. 前置校验:快速排除无效情况
// - s为null:直接无效
// - 长度为奇数:括号必须成对,奇数长度必然无法匹配
if (s == null || s.length() % 2 != 0) {
return false;
}
// 2. 初始化栈:存储左括号(Character类型,对应char)
// ArrayDeque是Java推荐的栈实现(替代旧的Stack类)
Deque<Character> stack = new ArrayDeque<>();
// 3. 遍历字符串的每个字符
for (char c : s.toCharArray()) {
// 4. 左括号:直接入栈(等待后续匹配右括号)
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
// 5. 右括号:核心匹配逻辑
// 5.1 栈为空但遇到右括号:无左括号可匹配,直接无效(比如输入 ")")
if (stack.isEmpty()) {
return false;
}
// 5.2 弹出栈顶的左括号,检查是否与当前右括号匹配
char top = stack.pop();
// 逐一判断:右括号必须和最近的左括号类型一致
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}
// 6. 最终校验:栈必须为空
// 若栈不为空,说明有未匹配的左括号(比如输入 "(()")
return stack.isEmpty();
}
}
时间复杂度:O(n)
空间复杂度:O(n)
4.2 最小栈(LeetCode 155)
题目描述
设计一个支持 push、pop、top 操作,并能在常数时间内检索到最小元素的栈。
示例:
java
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); // 返回 -3
minStack.pop();
minStack.top(); // 返回 0
minStack.getMin(); // 返回 -2
解题思路
使用两个栈:
- 数据栈:存储所有元素
- 最小栈:存储每个状态下的最小值
完整代码
java
import java.util.Deque;
import java.util.ArrayDeque;
class MinStack {
// 1. 定义两个栈,分工明确
private Deque<Integer> dataStack; // 数据栈:存储所有入栈的元素(常规栈功能)
private Deque<Integer> minStack; // 最小栈:栈顶始终是当前dataStack的最小值
// 2. 构造方法:初始化两个空栈
public MinStack() {
dataStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
// 3. 入栈操作(push)
public void push(int val) {
// 第一步:新元素先压入数据栈(常规栈操作)
dataStack.push(val);
// 第二步:更新最小栈(核心逻辑)
// 若最小栈为空(第一个元素),或新元素≤当前最小值(minStack栈顶),则压入最小栈
// 注意:用≤而不是<,是为了处理"重复最小值"的情况(比如连续入栈多个相同最小值)
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
// 4. 出栈操作(pop)
public void pop() {
// 第一步:弹出数据栈的栈顶元素(常规栈操作)
int val = dataStack.pop();
// 第二步:同步更新最小栈(核心逻辑)
// 如果弹出的元素恰好是当前最小值(等于minStack栈顶),则最小栈也弹出栈顶
// 保证minStack栈顶始终是剩余元素的最小值
if (val == minStack.peek()) {
minStack.pop();
}
}
// 5. 查栈顶操作(top):直接返回数据栈的栈顶元素
public int top() {
return dataStack.peek();
}
// 6. 获取最小值操作(getMin):直接返回最小栈的栈顶元素(O(1)时间)
public int getMin() {
return minStack.peek();
}
}
时间复杂度:所有操作都是 O(1)
空间复杂度:O(n)
4.3 逆波兰表达式求值(LeetCode 150)
题目描述
给你一个字符串数组 tokens,表示一个根据逆波兰表示法表示的算术表达式。
示例:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:((2 + 1) * 3) = 9
完整代码
java
import java.util.Deque;
import java.util.ArrayDeque;
public class Solution {
// 逆波兰表达式求值核心方法
public int evalRPN(String[] tokens) {
// 初始化栈,存储运算过程中的整数
Deque<Integer> stack = new ArrayDeque<>();
// 遍历每个token(表达式元素)
for (String token : tokens) {
// 判断当前元素是否为运算符
if (isOperator(token)) {
// 弹出两个操作数(注意顺序:先弹的是第二个操作数b,后弹的是第一个操作数a)
int b = stack.pop();
int a = stack.pop();
// 执行运算并将结果入栈
int result = calculate(a, b, token);
stack.push(result);
} else {
// 数字直接转为int入栈
stack.push(Integer.parseInt(token));
}
}
// 最终栈中仅剩的元素就是结果
return stack.pop();
}
// 判断是否为运算符(+、-、*、/)
private boolean isOperator(String token) {
return "+".equals(token) || "-".equals(token)
|| "*".equals(token) || "/".equals(token);
}
// 根据运算符执行对应的运算
private int calculate(int a, int b, String op) {
switch (op) {
case "+": return a + b;
case "-": return a - b;
case "*": return a * b;
case "/": return a / b; // 题目保证除数非0且结果为整数
default: return 0;
}
}
}
时间复杂度:O(n)
空间复杂度:O(n)
4.4 每日温度(LeetCode 739)
题目描述
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
解题思路
使用单调栈(栈底到栈顶递减):
- 遍历温度数组
- 如果当前温度大于栈顶索引对应的温度,说明找到了更高温度
- 弹出栈顶,计算天数差
- 将当前索引入栈
完整代码
java
import java.util.Deque;
import java.util.ArrayDeque;
public class Solution {
public int[] dailyTemperatures(int[] temperatures) {
// 1. 获取温度数组的长度n
int n = temperatures.length;
// 2. 初始化结果数组answer,默认值为0(未找到更高温度时就是0)
int[] answer = new int[n];
// 3. 初始化双端队列作为栈,存储的是温度数组的「索引」而非温度值(关键!)
// 为什么存索引?因为需要计算天数差(当前索引 - 之前索引)
Deque<Integer> stack = new ArrayDeque<>();
// 4. 遍历每一天的温度
for (int i = 0; i < n; i++) {
// 5. 核心逻辑:当栈不为空,且当前温度 > 栈顶索引对应的温度
// 说明找到了栈顶索引那一天的"更高温度天"
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
// 6. 弹出栈顶索引(这一天的更高温度已找到)
int prevIndex = stack.pop();
// 7. 计算天数差:当前天索引 - 栈顶天索引,存入结果数组
answer[prevIndex] = i - prevIndex;
}
// 8. 若栈为空,或当前温度 ≤ 栈顶温度,将当前索引压入栈(等待后续更高温度)
stack.push(i);
}
// 9. 返回结果数组(未找到更高温度的位置保持默认0)
return answer;
}
}
时间复杂度:O(n)
空间复杂度:O(n)
4.5 用栈实现队列(LeetCode 232)
题目描述
请你仅使用两个栈实现先入先出队列。
完整代码
java
import java.util.Deque;
import java.util.ArrayDeque;
class MyQueue {
// 1. 定义两个栈:分工明确
private Deque<Integer> inStack; // 入队栈:只负责接收新入队的元素(push操作)
private Deque<Integer> outStack; // 出队栈:只负责出队(pop)和查队首(peek)操作
// 2. 构造方法:初始化两个空栈
public MyQueue() {
inStack = new ArrayDeque<>();
outStack = new ArrayDeque<>();
}
// 3. 入队操作(push):直接压入入队栈
public void push(int x) {
inStack.push(x); // 新元素永远先进入inStack,符合栈的"后进先出"
}
// 4. 出队操作(pop):移除并返回队首元素
public int pop() {
// 核心逻辑:如果出队栈为空,先把入队栈的所有元素"倒"到出队栈
if (outStack.isEmpty()) {
// 循环将inStack的元素弹出,压入outStack(完成顺序反转)
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
// 此时outStack的栈顶就是队列的队首元素,弹出即可(符合队列FIFO)
return outStack.pop();
}
// 5. 查队首操作(peek):返回但不移除队首元素
public int peek() {
// 逻辑和pop完全一致:先保证outStack有元素(无则倒腾)
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
// 仅获取栈顶元素(不弹出)
return outStack.peek();
}
// 6. 判断队列是否为空
public boolean empty() {
// 只有两个栈都为空时,队列才是空的
return inStack.isEmpty() && outStack.isEmpty();
}
}
时间复杂度:push O(1),pop 和 peek 均摊 O(1)
空间复杂度:O(n)
五、易错点和最佳实践
5.1 常见错误
错误 1:使用过时的 Stack 类
java
// 不推荐写法
Stack<Integer> stack = new Stack<>();
// 推荐写法
Deque<Integer> stack = new ArrayDeque<>();
原因:Stack 继承 Vector,性能差,破坏封装性,官方已不推荐使用。
错误 2:栈为空时调用 pop()
java
Deque<Integer> stack = new ArrayDeque<>();
// 错误:栈为空时调用 pop()
int top = stack.pop(); // 抛出 NoSuchElementException
// 正确:先判断是否为空
if (!stack.isEmpty()) {
int top = stack.pop();
}
// 或者使用 poll()(返回 null 而不抛异常)
Integer top = stack.poll();
if (top != null) {
// 处理
}
错误 3:混淆 peek() 和 pop()
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
// peek() 只查看,不移除
int top1 = stack.peek(); // 返回 3
int top2 = stack.peek(); // 还是返回 3
// pop() 查看并移除
int top3 = stack.pop(); // 返回 3
int top4 = stack.pop(); // 返回 2(栈顶已变化)
错误 4:在 ArrayDeque 中存储 null
java
Deque<Integer> stack = new ArrayDeque<>();
// 错误:ArrayDeque 不允许 null
stack.push(null); // 抛出 NullPointerException
// 如果需要存储 null,使用 LinkedList
Deque<Integer> stack2 = new LinkedList<>();
stack2.push(null); // 允许
错误 5:遍历栈时修改栈
java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
// 错误:使用迭代器遍历时修改栈
for (Integer num : stack) {
stack.pop(); // 抛出 ConcurrentModificationException
}
// 正确:使用 while 循环
while (!stack.isEmpty()) {
int num = stack.pop();
System.out.println(num);
}
5.2 性能优化技巧
选择合适的初始容量
java
// ArrayDeque 默认初始容量为 16
Deque<Integer> stack1 = new ArrayDeque<>();
// 如果知道大概大小,可以指定初始容量,避免扩容
Deque<Integer> stack2 = new ArrayDeque<>(1000);
扩容机制:
- ArrayDeque 扩容时容量翻倍
- 扩容需要复制数组,时间复杂度 O(n)
- 预估容量可以减少扩容次数
避免频繁装箱拆箱
java
// 性能较差:频繁装箱拆箱
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < 1000000; i++) {
stack.push(i); // int → Integer(装箱)
}
while (!stack.isEmpty()) {
int num = stack.pop(); // Integer → int(拆箱)
}
// 如果对性能要求极高,可以自己实现基本类型栈
class IntStack {
private int[] data;
private int top;
public IntStack(int capacity) {
data = new int[capacity];
top = -1;
}
public void push(int val) {
data[++top] = val;
}
public int pop() {
return data[top--];
}
public int peek() {
return data[top];
}
public boolean isEmpty() {
return top == -1;
}
}
复用栈对象
java
// 性能较差:频繁创建栈对象
for (int i = 0; i < 1000; i++) {
Deque<Integer> stack = new ArrayDeque<>();
// 使用栈
}
// 性能较好:复用栈对象
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < 1000; i++) {
stack.clear(); // 清空栈
// 使用栈
}
六、线程安全的栈
6.1为什么需要线程安全
在多线程环境下,多个线程同时操作栈可能导致数据不一致。
java
// 线程不安全
Deque<Integer> stack = new ArrayDeque<>();
// 线程 1
stack.push(1);
// 线程 2(同时执行)
stack.push(2);
// 可能导致数据丢失或异常
方案一:ConcurrentLinkedDeque
java
import java.util.concurrent.ConcurrentLinkedDeque;
// 线程安全的栈
Deque<Integer> stack = new ConcurrentLinkedDeque<>();
// 多线程环境下安全使用
stack.push(1);
stack.pop();
特点:
- 基于 CAS(Compare-And-Swap)实现,无锁
- 性能优于 synchronized
- 适合高并发场景
方案二:Collections.synchronizedDeque()
java
import java.util.Collections;
import java.util.Deque;
import java.util.ArrayDeque;
// 包装为线程安全的栈
Deque<Integer> stack = Collections.synchronizedDeque(new ArrayDeque<>());
// 多线程环境下安全使用
stack.push(1);
stack.pop();
特点:
- 基于 synchronized 实现
- 性能不如 ConcurrentLinkedDeque
- 适合低并发场景
6.2 线程安全方案对比
| 方案 | 底层实现 | 性能 | 推荐度 | 适用场景 |
|---|---|---|---|---|
| ConcurrentLinkedDeque | CAS 无锁 | 高 | 强烈推荐 | 高并发 |
| synchronizedDeque | synchronized | 中等 | 推荐 | 低并发 |
| Stack | synchronized | 低 | 不推荐 | 无 |
七、总结
7.1 核心要点
- 栈是 **LIFO(后进先出)**的数据结构
- 推荐使用 Deque + ArrayDeque 实现栈
- 不推荐使用 Stack 类(已过时)
- ArrayDeque 性能最好,LinkedList 适合频繁插入删除
- 核心方法:push()、pop()、peek()、isEmpty()、size()
- 栈为空时调用 pop() 会抛异常,使用 poll() 更安全
- ArrayDeque 不允许 null,LinkedList 允许
- 多线程环境使用 ConcurrentLinkedDeque
7.2 方法速查表
| 操作 | 方法 | 时间复杂度 | 说明 |
|---|---|---|---|
| 入栈 | push(e) | O(1) | 在栈顶插入 |
| 出栈 | pop() | O(1) | 移除并返回栈顶 |
| 查看栈顶 | peek() | O(1) | 返回栈顶但不移除 |
| 安全出栈 | poll() | O(1) | 栈为空返回 null |
| 判断空 | isEmpty() | O(1) | 栈是否为空 |
| 获取大小 | size() | O(1) | 栈中元素个数 |
| 清空 | clear() | O(n) | 清空栈 |
| 包含 | contains(o) | O(n) | 是否包含元素 |
| 转数组 | toArray() | O(n) | 转换为数组 |
7.3 选择指南
单线程环境:
java
// 首选:ArrayDeque(性能最好)
Deque<Integer> stack = new ArrayDeque<>();
// 备选:LinkedList(需要存储 null 时)
Deque<Integer> stack = new LinkedList<>();
多线程环境:
java
// 首选:ConcurrentLinkedDeque(高并发)
Deque<Integer> stack = new ConcurrentLinkedDeque<>();
// 备选:synchronizedDeque(低并发)
Deque<Integer> stack = Collections.synchronizedDeque(new ArrayDeque<>());
7.4 同类题推荐
| 题号 | 题目 | 难度 | 核心技巧 |
|---|---|---|---|
| 20 | 有效的括号 | 简单 | 栈匹配 |
| 155 | 最小栈 | 中等 | 辅助栈 |
| 150 | 逆波兰表达式求值 | 中等 | 栈计算 |
| 232 | 用栈实现队列 | 简单 | 双栈 |
| 225 | 用队列实现栈 | 简单 | 单队列 |
| 496 | 下一个更大元素 I | 简单 | 单调栈 |
| 739 | 每日温度 | 中等 | 单调栈 |
| 84 | 柱状图中最大的矩形 | 困难 | 单调栈 |
| 42 | 接雨水 | 困难 | 单调栈 |
| 394 | 字符串解码 | 中等 | 栈辅助 |
7.5 完整代码示例
java
import java.util.Deque;
import java.util.ArrayDeque;
public class StackExample {
public static void main(String[] args) {
// 创建栈
Deque<Integer> stack = new ArrayDeque<>();
// 入栈
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("栈:" + stack); // [3, 2, 1]
// 查看栈顶
System.out.println("栈顶:" + stack.peek()); // 3
// 出栈
System.out.println("出栈:" + stack.pop()); // 3
System.out.println("栈:" + stack); // [2, 1]
// 判断是否为空
System.out.println("是否为空:" + stack.isEmpty()); // false
// 获取大小
System.out.println("大小:" + stack.size()); // 2
// 遍历栈(不移除元素)
System.out.println("遍历栈:");
for (Integer num : stack) {
System.out.println(num);
}
// 清空栈
stack.clear();
System.out.println("清空后是否为空:" + stack.isEmpty()); // true
}
}
作者:[识君啊]
不要做API的搬运工,要做原理的探索者!