Java 栈 - 附LeetCode 经典题解

Java 栈 - 附LeetCode 经典题解

从底层原理到实战应用,彻底掌握 Java 栈的使用

一、栈的基础概念

1.1 什么是栈

想象一摞盘子:只能在最上面放盘子(入栈 push),只能拿最上面的盘子(出栈 pop),只能看最上面的盘子(查看栈顶 peek)。这就是栈的核心特性:后进先出(LIFO - Last In First Out)

tex 复制代码
栈的操作示意:
    ┌───┐
    │ 3 │ ← 栈顶(最后进来的)
    ├───┤
    │ 2 │
    ├───┤
    │ 1 │ ← 栈底(最先进来的)
    └───┘

栈是一种线性数据结构,只允许在一端(栈顶)进行插入和删除操作。核心特性包括:

  • LIFO:后进先出
  • 单端操作:只能在栈顶操作
  • 受限访问:不能随机访问中间元素

1.2 栈的应用场景

栈在计算机科学中应用广泛:

  1. 函数调用栈:方法调用和返回
  2. 表达式求值:中缀转后缀、计算器实现
  3. 括号匹配:检查括号是否配对
  4. 浏览器历史:前进/后退功能
  5. 撤销操作:Ctrl+Z 功能实现
  6. 深度优先搜索(DFS):图和树的遍历
  7. 递归转迭代:用栈模拟递归调用

二、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 的优势

  1. 性能最好:基于循环数组,无锁开销
  2. 内存效率高:没有节点对象的额外开销
  3. 扩容机制:自动扩容,初始容量 16
  4. 不允许 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 的特点:

  1. 基于双向链表:每个元素是一个 Node 对象
  2. 适合频繁插入删除:不需要移动元素
  3. 允许 null:可以存储 null 值
  4. 内存开销大:每个节点需要额外的 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

解题思路

典型的栈应用:

  1. 遇到左括号,入栈
  2. 遇到右括号,检查栈顶是否匹配
  3. 最后检查栈是否为空

完整代码

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

解题思路

使用两个栈:

  1. 数据栈:存储所有元素
  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]

解题思路

使用单调栈(栈底到栈顶递减):

  1. 遍历温度数组
  2. 如果当前温度大于栈顶索引对应的温度,说明找到了更高温度
  3. 弹出栈顶,计算天数差
  4. 将当前索引入栈

完整代码

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 核心要点

  1. 栈是 **LIFO(后进先出)**的数据结构
  2. 推荐使用 Deque + ArrayDeque 实现栈
  3. 不推荐使用 Stack 类(已过时)
  4. ArrayDeque 性能最好,LinkedList 适合频繁插入删除
  5. 核心方法:push()、pop()、peek()、isEmpty()、size()
  6. 栈为空时调用 pop() 会抛异常,使用 poll() 更安全
  7. ArrayDeque 不允许 null,LinkedList 允许
  8. 多线程环境使用 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的搬运工,要做原理的探索者!

相关推荐
芒克芒克1 小时前
深入浅出BlockingQueue(二)
java
shehuiyuelaiyuehao1 小时前
关于hashset和hashmap,还有treeset和treemap,四个的关系
java·开发语言
马尔代夫哈哈哈1 小时前
Spring AOP
java·后端·spring
only-qi1 小时前
Java 包装器模式:告别“类爆炸“
java·开发语言
Yweir1 小时前
Java 接口测试框架 Restassured
java·开发语言
wangbing11251 小时前
开发指南141-类和字节数组转换
java·服务器·前端
~央千澈~1 小时前
抖音弹幕游戏开发之第15集:添加配置文件·优雅草云桧·卓伊凡
java·前端·python
肖。35487870941 小时前
html中onclick误区,后续变量会更改怎么办?
android·java·javascript·css·html
郝学胜-神的一滴1 小时前
Effective Modern C++ 条款39:一次事件通信的优雅解决方案
开发语言·数据结构·c++·算法·多线程·并发