上一篇讲了线性表(顺序表和链表),这一篇讲线性表的两种特殊形式------栈(Stack)和队列(Queue)。它们在 408 考研和面试中出现频率极高。

一、栈------后进先出

1. 什么是栈

栈(Stack) 是限定只能在一端进行插入和删除操作的线性表。

复制代码

允许操作的一端 → 栈顶(Top)

不允许操作的一端 → 栈底(Bottom)

插入 → 入栈(Push)

删除 → 出栈(Pop)

← 出栈

┌───┐

│ 5 │ ← 栈顶

├───┤

│ 4 │

├───┤

│ 3 │

├───┤

│ 2 │

├───┤

│ 1 │ ← 栈底

└───┘

push(6) → 放到栈顶

特点:后进先出(LIFO, Last In First Out)

生活中的例子:

一叠盘子------你只能从上面拿(后放上去的先用)

浏览器的后退------最后访问的页面最先回退

函数调用------最里层的函数最先返回

2. 顺序栈(数组实现)

java

复制代码

public class ArrayStack {

private static final int DEFAULT_CAPACITY = 10;

private E[] data;

private int top; // 栈顶指针(指向当前栈顶元素位置)

public ArrayStack() {

data = (E[]) new Object[DEFAULT_CAPACITY];

top = -1; // 初始为空栈

}

// 入栈

public void push(E element) {

if (top == data.length - 1) {

grow(); // 扩容

}

data[++top] = element;

}

// 出栈

public E pop() {

if (isEmpty()) throw new EmptyStackException();

E element = data[top];

data[top--] = null; // 清空引用,帮助 GC

return element;

}

// 查看栈顶(不删除)

public E peek() {

if (isEmpty()) throw new EmptyStackException();

return data[top];

}

public boolean isEmpty() {

return top == -1;

}

public int size() {

return top + 1;

}

}

时间复杂度: push/pop/peek 都是 O(1)。

3. 链栈(链表实现)

java

复制代码

public class LinkedStack {

private Node top; // 栈顶(链表头)

private int size;

private static class Node {

E data;

Node next;

Node(E data) { this.data = data; }

}

public void push(E element) {

Node newNode = new Node<>(element);

newNode.next = top; // 新节点指向旧栈顶

top = newNode; // 新节点成为栈顶

size++;

}

public E pop() {

if (isEmpty()) throw new EmptyStackException();

E data = top.data;

top = top.next; // 栈顶下移

size--;

return data;

}

public E peek() {

if (isEmpty()) throw new EmptyStackException();

return top.data;

}

public boolean isEmpty() {

return top == null;

}

}

注意: 链栈的 push/pop 都是在链表头部操作,不需要遍历,时间复杂度 O(1)。

二、队列------先进先出

1. 什么是队列

队列(Queue) 是限定在一端插入、另一端删除的线性表。

复制代码

插入(队尾)→ ┌───┬───┬───┬───┬───┐ → 删除(队头)

│ 1 │ 2 │ 3 │ 4 │ 5 │

└───┴───┴───┴───┴───┘

front ↑ ↑ rear

特点:先进先出(FIFO, First In First Out)

生活中的例子:

排队买奶茶------先来的先买到

打印机任务队列------先提交的先打印

消息队列------生产者发送,消费者按顺序处理

2. 顺序队列的问题

java

复制代码

// 直接使用数组实现队列的问题:

// 出队时 front 后移,导致数组前面的空间被浪费

初始: front=0, rear=0 [ ][ ][ ][ ][ ]

A入队: [A][ ][ ][ ][ ]

B入队: [A][B][ ][ ][ ]

A出队: front=1 [ ][B][ ][ ][ ] ← 下标0的空间浪费了

解决方案:循环队列

3. 循环队列

复制代码

┌──────────────────────┐

│ rear │

│ ↓ │

│ ┌──┬──┬──┬──┬──┐ │

│ │ │ │E │F │G │ │

│ ├──┼──┼──┼──┼──┤ │

│ │D │ │ │ │ │ │

│ └──┴──┴──┴──┴──┘ │

│ ↑ │

│ front │

└──────────────────────┘

java

复制代码

public class CircularQueue {

private E[] data;

private int front; // 队头指针

private int rear; // 队尾指针(指向下一个插入位置)

private int size;

private int capacity;

public CircularQueue(int capacity) {

this.capacity = capacity;

data = (E[]) new Object[capacity];

front = 0;

rear = 0;

size = 0;

}

// 入队

public boolean enqueue(E element) {

if (isFull()) return false;

data[rear] = element;

rear = (rear + 1) % capacity; // 循环

size++;

return true;

}

// 出队

public E dequeue() {

if (isEmpty()) return null;

E element = data[front];

data[front] = null;

front = (front + 1) % capacity; // 循环

size--;

return element;

}

public boolean isEmpty() {

return size == 0;

}

public boolean isFull() {

return size == capacity;

}

}

关键: rear = (rear + 1) % capacity 实现循环------到达数组末尾后回到开头。

4. 链式队列

java

复制代码

public class LinkedQueue {

private Node front; // 队头(出队)

private Node rear; // 队尾(入队)

private int size;

public void enqueue(E element) {

Node newNode = new Node<>(element);

if (isEmpty()) {

front = rear = newNode;

} else {

rear.next = newNode;

rear = newNode;

}

size++;

}

public E dequeue() {

if (isEmpty()) return null;

E data = front.data;

front = front.next;

if (front == null) rear = null; // 队列变空

size--;

return data;

}

}

三、栈和队列的对比

对比

队列

规则

后进先出(LIFO)

先进先出(FIFO)

插入/删除位置

同一端(栈顶)

两端(队尾/队头)

常用实现

数组(顺序栈)

循环数组(循环队列)

适用场景

递归转非递归、括号匹配

排队系统、BFS、消息队列

四、408 考研经典考题

题1:括号匹配

java

复制代码

public boolean isValid(String s) {

Stack 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 != '(') return false;

if (c == ']' && top != '[') return false;

if (c == '}' && top != '{') return false;

}

}

return stack.isEmpty(); // 所有左括号都匹配完

}

题2:用两个栈实现队列

java

复制代码

class MyQueue {

private Stack inStack; // 入队栈

private Stack outStack; // 出队栈

public MyQueue() {

inStack = new Stack<>();

outStack = new Stack<>();

}

public void push(int x) {

inStack.push(x); // 直接压入入队栈

}

public int pop() {

if (outStack.isEmpty()) {

// 把入队栈的元素全部倒入出队栈

while (!inStack.isEmpty()) {

outStack.push(inStack.pop());

}

}

return outStack.pop();

}

}

原理: 入队时放入 inStack,出队时从 outStack 取。outStack 空了就把 inStack 全部倒过来------倒一次后元素的顺序就变成了队列顺序。

题3:循环队列判空判满

复制代码

两种判断方式:

方法1:size 字段(推荐,简单明了)

isEmpty() → size == 0

isFull() → size == capacity

方法2:牺牲一个存储单元

空 → rear == front

满 → (rear + 1) % capacity == front

题4:用队列实现栈(了解即可)

java

复制代码

class MyStack {

private Queue queue;

public void push(int x) {

queue.offer(x);

// 把前面的元素移到后面,让新元素在队头

for (int i = 0; i < queue.size() - 1; i++) {

queue.offer(queue.poll());

}

}

public int pop() {

return queue.poll();

}

}

五、实际开发中的应用

复制代码

栈的应用:

JVM 虚拟机栈 → 方法调用和返回

表达式求值 → 中缀转后缀

撤销操作 → Ctrl+Z

队列的应用:

线程池任务队列 → 等待执行的任务

消息队列 MQ → 异步解耦

树的层序遍历 → BFS

六、Java 中的栈和队列

java

复制代码

// 栈(Java 官方推荐用 Deque 代替 Stack)

Deque stack = new ArrayDeque<>();

stack.push("A"); // 入栈

stack.pop(); // 出栈

stack.peek(); // 查看栈顶

// 队列

Queue queue = new LinkedList<>();

queue.offer("A"); // 入队

queue.poll(); // 出队

queue.peek(); // 查看队头

// 双端队列(两端都可操作)

Deque deque = new ArrayDeque<>();

deque.addFirst("A");

deque.addLast("B");

deque.removeFirst();

deque.removeLast();

总结: 栈和队列其实就是限制了操作位置的线性表。栈只在一端操作(LIFO),队列在两端操作(FIFO)。代码实现时注意循环队列的取模运算和判空判满条件。

💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。