数据结构——栈与队列:原理、实现与经典应用

上一篇讲了线性表(顺序表和链表),这一篇讲线性表的两种特殊形式------栈(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<E> {
    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<E> {
    private Node<E> top;  // 栈顶(链表头)
    private int size;

    private static class Node<E> {
        E data;
        Node<E> next;
        Node(E data) { this.data = data; }
    }

    public void push(E element) {
        Node<E> 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<E> {
    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<E> {
    private Node<E> front;  // 队头(出队)
    private Node<E> rear;   // 队尾(入队)
    private int size;

    public void enqueue(E element) {
        Node<E> 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<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 != '(') return false;
            if (c == ']' && top != '[') return false;
            if (c == '}' && top != '{') return false;
        }
    }

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

题2:用两个栈实现队列

java 复制代码
class MyQueue {
    private Stack<Integer> inStack;   // 入队栈
    private Stack<Integer> 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<Integer> 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<String> stack = new ArrayDeque<>();
stack.push("A");     // 入栈
stack.pop();         // 出栈
stack.peek();        // 查看栈顶

// 队列
Queue<String> queue = new LinkedList<>();
queue.offer("A");    // 入队
queue.poll();        // 出队
queue.peek();        // 查看队头

// 双端队列(两端都可操作)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("A");
deque.addLast("B");
deque.removeFirst();
deque.removeLast();

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


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