Java数据结构——栈(Stack)详解

Java数据结构------栈(Stack)详解

学数据结构的时候,栈算是比较简单但特别实用的一个。这篇文章把栈的概念、常用方法、手动实现以及几个经典应用场景都过了一遍,希望能帮到正在复习的朋友。


一、栈是什么?

栈是一种受限的线性表 ,说"受限"是因为它只允许在固定的一端进行插入和删除操作。

这一端叫栈顶 (Top),另一端叫栈底(Bottom)。

栈里元素的操作遵循一个核心原则------后进先出(LIFO,Last In First Out)

什么意思呢?就像你桌子上叠了一摞盘子,最后放上去的那个盘子,你肯定最先拿走。这就是后进先出。

栈的相关概念:

  • 压栈(Push):也叫入栈,往栈顶放一个元素
  • 出栈(Pop):把栈顶的元素移除
  • 栈顶:操作都在这头进行
  • 栈底:封死的那头,不动

画个图可能更直观:

3 最后进去,也最先出来。

二、栈的常用方法

Java 里 java.util.Stack 类直接提供了这些操作,用起来很简单:

方法 说明
Stack() 构造一个空栈
E push(E e) 把 e 压入栈顶,并返回 e
E pop() 弹出栈顶元素并返回
E peek() 查看栈顶元素(不弹出)
int size() 返回栈中元素个数
boolean isEmpty() 判断栈是否为空

快速演示一下:

java 复制代码
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);

System.out.println(stack.peek()); // 3,栈顶元素
System.out.println(stack.pop());  // 3,弹出栈顶
System.out.println(stack.size()); // 2
System.out.println(stack.isEmpty()); // false

没什么难度,关键是要理解什么时候该用栈,这个后面会讲到。## 三、自己动手实现一个栈

不过,仅仅会调用 API 还不够,面试中常常要求手写实现。用数组作为底层结构来实现栈,思路非常直观:

java 复制代码
public class MyStack {
    private int[] array;
    private int size;

    public MyStack() {
        array = new int[3]; // 初始容量给 3,随便给个值
    }

    // 获取栈顶元素
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法获取栈顶元素");
        }
        return array[size - 1];
    }

    // 入栈
    public int push(int value) {
        if (isFull()) {
            expand(); // 满了就扩容
        }
        array[size] = value;
        size++;
        return array[size - 1];
    }

    // 出栈
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法出栈");
        }
        int tmp = array[size - 1];
        size--;
        return tmp;
    }

    // 扩容(2 倍扩容,和 ArrayList 的思路一样)
    private void expand() {
        array = Arrays.copyOf(array, array.length * 2);
    }

    // 判满
    private boolean isFull() {
        return size == array.length;
    }

    // 判空
    public boolean isEmpty() {
        return size == 0;
    }

    // 获取元素个数
    public int getSize() {
        return size;
    }
}

这里有几个点需要注意:

  1. 扩容策略:这里用的是 2 倍扩容,和 ArrayList 是一个思路。当然你也可以用其他倍数,但 2 倍是比较常见的做法。
  2. 边界检查peekpop 之前都要先判断栈是不是空的,不然会数组越界。
  3. pop 不需要真正删除数据 :只要 size-- 就行了,下次 push 会直接覆盖旧值。## 四、栈的经典应用场景

栈的应用场景还挺多的,这里挑几个最常考的来说。

4.1 递归转非递归

这个思想很重要------递归的本质其实就是系统帮你维护了一个函数调用栈。每次递归调用,系统都会把当前的状态压栈;递归返回的时候,再依次出栈。

所以理论上,任何递归都能改写成用栈模拟的循环形式。有些场景下这样做还能避免栈溢出。

4.2 括号匹配

LeetCode 20. 有效的括号

这道题是栈的经典入门题。思路:

  1. 创建一个栈
  2. 遍历字符串中的每个字符
    • 遇到左括号,就压入栈中
    • 遇到右括号 ,就 peek 栈顶看匹不匹配
      • 匹配就 pop,继续
      • 不匹配直接返回 false(比如栈顶是 [ 但当前是 )
  3. 遍历完了之后,栈为空说明全部匹配上了,返回 true
java 复制代码
public boolean isValid(String s) {
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);
        } else {
            if (stack.isEmpty()) return false;
            char top = stack.pop();
            if ((c == ')' && top != '(') ||
                (c == ']' && top != '[') ||
                (c == '}' && top != '{')) {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

这题不难,但一定要考虑边界情况:比如字符串为空、右括号比左括号多、左括号比右括号多。

4.3 逆波兰表达式求值

LeetCode 150. 逆波兰表达式求值

先说下什么是逆波兰表达式(后缀表达式)。

我们平时写的 a + b * c中缀表达式 ,运算符在两个操作数中间。而逆波兰表达式把运算符放到后面,上面的例子转换后是 a b c * +

转换方法

  1. 从左到右按运算顺序加括号:(a + (b * c))
  2. 把运算符移到对应括号的后面:(a (b c) *) +
  3. 去掉括号:a b c * +

求值方法(用栈):

  1. 从左到右遍历逆波兰表达式
  2. 遇到数字就入栈
  3. 遇到运算符就弹出两个元素
    • 先弹出的放运算符右边,后弹出的放左边(这个顺序别搞反了)
    • 计算结果,再入栈
  4. 遍历完,栈里剩下的那个数就是答案

举个例子,1 3 4 * +

复制代码
遍历 "1"   → 入栈,栈:[1]
遍历 "3"   → 入栈,栈:[1, 3]
遍历 "4"   → 入栈,栈:[1, 3, 4]
遍历 "*"   → 弹出 4 和 3,算 3*4=12,入栈,栈:[1, 12]
遍历 "+"   → 弹出 12 和 1,算 1+12=13,入栈,栈:[13]

结果:13
java 复制代码
public int evalRPN(String[] tokens) {
    Stack<Integer> stack = new Stack<>();
    for (String token : tokens) {
        if (token.equals("+") || token.equals("-") ||
            token.equals("*") || token.equals("/")) {
            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();
}

注意那个弹出顺序:b 先弹出来放右边,a 后弹出来放左边。做减法和除法的时候搞反了结果就不对了。

4.4 最小栈

LeetCode 155. 最小栈

题目要求:设计一个栈,除了正常的 pushpoptop 操作之外,还要能在 O(1) 时间内获取栈中的最小元素。

思路是用两个栈

  • stack:正常存所有元素
  • minStack:辅助栈,栈顶始终保存当前 stack 中的最小值

push 操作

  • 元素压入 stack
  • 如果 minStack 为空,或者当前元素 minStack 栈顶,也压入 minStack

pop 操作

  • 弹出 stack 栈顶
  • 如果弹出的元素等于 minStack 栈顶,minStack 也弹出

getMin 操作

  • 直接返回 minStack 栈顶就行
java 复制代码
class MinStack {
    private Stack<Integer> stack;
    private Stack<Integer> minStack;

    public MinStack() {
        stack = new Stack<>();
        minStack = new Stack<>();
    }

    public void push(int val) {
        stack.push(val);
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }

    public void pop() {
        if (stack.pop().equals(minStack.peek())) {
            minStack.pop();
        }
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

这里有个很容易踩的坑push 的时候判断条件要用 <= 而不是 <

为什么?假设栈里压入了 5 个 1,如果 minStack 只记录了一个 1:

  1. pop 的时候,stack 弹出一个 1,minStack 也把唯一的 1 弹出了
  2. stack 里还剩 4 个 1,实际最小值还是 1
  3. 这时候再调 getMinminStack 栈顶可能已经变成 2 了,结果就错了

所以遇到重复最小值的时候,每个都得往 minStack 里存一份,用 <= 就能解决这个问题。

4.5 用栈实现队列

LeetCode 232. 用栈实现队列

这个题挺有意思的。栈是后进先出,队列是先进先出,顺序刚好相反。但用两个栈配合就能实现。

核心思路:

  • stackIn:负责接收入队元素
  • stackOut:负责出队

入队 :直接 pushstackIn

出队

  • 如果 stackOut 不为空,直接弹出栈顶
  • 如果 stackOut 为空,把 stackIn所有 元素依次弹出并压入 stackOut,然后再弹出 stackOut 栈顶

为什么要这么搞?因为元素从 stackIn 倒入 stackOut 的过程中,顺序就反过来了。stackIn 栈底的元素(最早入队的)变成了 stackOut 的栈顶(最先出队的),刚好满足队列的 FIFO 特性。

java 复制代码
class MyQueue {
    private Stack<Integer> stackIn;
    private Stack<Integer> stackOut;

    public MyQueue() {
        stackIn = new Stack<>();
        stackOut = new Stack<>();
    }

    public void push(int x) {
        stackIn.push(x);
    }

    public int pop() {
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.pop();
    }

    public int peek() {
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.peek();
    }

    public boolean empty() {
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
}

有个关键点 :只有 stackOut 为空的时候才倒入,不能每次 pop 都倒。如果 stackOut 里还有元素就倒入,会把已有的顺序打乱。

时间复杂度方面,虽然单次 pop 最坏是 O(n)(需要倒入),但均摊下来每个元素最多被移动两次(进 stackIn 一次,倒入 stackOut 一次),所以均摊时间复杂度是 O(1)


五、总结

栈这个东西说简单也简单,说重要也确实重要。几个要点回顾一下:

  1. 栈是后进先出(LIFO)的线性表
  2. 核心操作就四个:pushpoppeekisEmpty
  3. 手写实现的话,底层用数组,注意扩容和边界检查
  4. 经典应用场景:括号匹配、逆波兰表达式、最小栈、栈实现队列

这几个题刷熟练了,栈这块基本就没什么问题了。后面有时间再写一篇队列的,和栈对照着看效果更好。## 五、总结

栈这个东西说简单也简单,说重要也确实重要。几个要点回顾一下:

  1. 栈是后进先出(LIFO)的线性表
  2. 核心操作就四个:pushpoppeekisEmpty
  3. 手写实现的话,底层用数组,注意扩容和边界检查
  4. 经典应用场景:括号匹配、逆波兰表达式、最小栈、栈实现队列

这几个题刷熟练了,栈这块基本就没什么问题了。后面有时间再写一篇队列的,和栈对照着看效果更好。

相关推荐
TechWayfarer1 小时前
网络安全视角:利用IP定位API接口识别机房与基站流量(合规风控篇)
开发语言·网络·数据库·python·安全·网络安全
Makoto_Kimur1 小时前
Java 后端面试场景题:页面刷新后一直转圈,应该怎么排查?
java·开发语言·面试
小陶来咯1 小时前
aimrt中间件的使用
开发语言·qt·中间件
神仙别闹1 小时前
基于C语言实现(控制台)学生信息管理系统
c语言·开发语言
牢姐与蒯1 小时前
C++数据结构之红黑树
数据结构
ch.ju1 小时前
Java Programming Chapter 3——Default value of array
java·开发语言
YL200404261 小时前
041二叉树的层序遍历
数据结构·leetcode·bfs
aini_lovee1 小时前
STM32 上实现 SD 卡读取 JPEG 解码 TFT 显示
开发语言·stm32
谙弆悕博士1 小时前
【附C语言源码】C语言 栈结构 实现及其扩展操作
c语言·开发语言·数据结构·算法·链表·指针·