栈与队列深度解析:概念、实现与应用

在数据结构的世界里,栈和队列是两种极为基础且应用广泛的线性表结构。它们凭借独特的元素操作规则,在算法设计、系统开发等领域发挥着关键作用。本文将基于经典数据结构理论,从概念定义、常用方法、模拟实现到实际应用,全方位剖析栈与队列,帮助大家夯实基础,应对各类编程场景。

一、栈(Stack):后进先出的“数据容器”

栈就像我们日常生活中叠放的盘子——只能从最顶端拿取盘子,也只能将新盘子放在最顶端。这种“后进先出”的特性,是栈的核心标志。

1.1 栈的核心概念

  • 栈顶:允许插入和删除元素的一端,是栈的“活动端”。
  • 栈底:固定不动的一端,元素一旦压入栈底,需等上方所有元素出栈后才能访问。
  • 压栈(Push):将元素插入栈顶的操作。
  • 出栈(Pop):将栈顶元素删除并返回的操作。

用一个简单的示例理解栈的操作流程:
若依次执行 Push(7) → Push(1) → Push(8) → Push(2),栈内元素从栈底到栈顶的顺序为 7 → 1 → 8 → 2;再依次执行 Pop() → Pop() → Pop(),出栈元素顺序为 2 → 8 → 1,剩余栈顶元素为 7

1.2 栈的常用方法(Java实现)

在Java中,java.util.Stack 类提供了栈的基础实现,常用方法如下表所示:

方法名功能描述返回值
Stack()构造一个空栈
push(E e)将元素e压入栈顶被压入的元素e
pop()移除并返回栈顶元素栈顶元素(若栈空会抛异常)
peek()获取栈顶元素(不删除)栈顶元素(若栈空会抛异常)
size()获取栈中有效元素个数整数(元素个数)
empty()判断栈是否为空true(空栈)/false(非空)
代码示例:栈的基础使用
import java.util.Stack;

public class StackDemo {
    public static void main(String[] args) {
        // 1. 初始化栈
        Stack<Integer> stack = new Stack<>();
        
        // 2. 压栈操作
        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.push(4);
        System.out.println("栈中元素个数:" + stack.size()); // 输出:4
        System.out.println("当前栈顶元素:" + stack.peek()); // 输出:4
        
        // 3. 出栈操作
        stack.pop(); // 4出栈
        System.out.println("出栈后栈顶元素:" + stack.pop()); // 输出:3(3出栈)
        
        // 4. 判断栈是否为空
        if (stack.empty()) {
            System.out.println("栈空");
        } else {
            System.out.println("剩余元素个数:" + stack.size()); // 输出:2(剩余1、2)
        }
    }
}

1.3 栈的模拟实现(基于数组)

Java中的Stack类继承自Vector(线程安全的动态数组),我们可以基于普通数组手动模拟栈的实现,核心思路是:

  1. 用数组存储元素,size记录有效元素个数;
  2. 压栈前检查数组容量,不足则扩容(如2倍扩容);
  3. 出栈/获取栈顶元素前检查栈是否为空,空则抛异常。
代码实现:自定义数组栈
import java.util.Arrays;

public class MyStack {
    // 存储元素的数组
    private int[] array;
    // 栈中有效元素个数
    private int size;

    // 构造方法:初始化数组容量为3
    public MyStack() {
        array = new int[3];
    }

    // 压栈操作
    public int push(int e) {
        // 检查容量,不足则扩容
        ensureCapacity();
        array[size++] = e;
        return e;
    }

    // 出栈操作
    public int pop() {
        // 栈空则抛异常
        if (empty()) {
            throw new RuntimeException("栈为空,无法执行出栈操作");
        }
        int topVal = peek();
        size--; // 直接通过size减少实现“删除”(后续元素会覆盖)
        return topVal;
    }

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

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

    // 判断栈是否为空
    public boolean empty() {
        return size == 0;
    }

    // 扩容方法:当元素个数等于数组长度时,扩容为原来的2倍
    private void ensureCapacity() {
        if (size == array.length) {
            array = Arrays.copyOf(array, size * 2);
        }
    }

    // 测试
    public static void main(String[] args) {
        MyStack stack = new MyStack();
        stack.push(5);
        stack.push(6);
        stack.push(7);
        stack.push(8); // 触发扩容(数组长度从3变为6)
        System.out.println("栈顶元素:" + stack.peek()); // 输出:8
        System.out.println("出栈元素:" + stack.pop()); // 输出:8
        System.out.println("剩余元素个数:" + stack.size()); // 输出:3
    }
}

1.4 栈的典型应用场景

栈的“后进先出”特性使其在诸多场景中发挥不可替代的作用,以下是常见应用:

1. 改变元素序列(栈的进出序列问题)

这是面试中高频出现的题型,核心是判断“出栈序列是否合法”。
例题1:若进栈序列为 1,2,3,4,进栈过程中可出栈,下列不可能的出栈序列是( )
A. 1,4,3,2 B. 2,3,4,1 C. 3,1,4,2 D. 3,4,2,1
解析

  • 选项C中,3先出栈,说明此时栈内已有1,2(栈底到栈顶);3出栈后,栈顶为2, next出栈元素只能是2,而非1,因此C不合法。
2. 将递归转化为循环(以“逆序打印链表”为例)

递归的本质是系统栈的调用,当递归深度过大时可能导致栈溢出,此时可用手动栈将递归改为循环。

递归实现

public void printListRecursive(Node head) {
    if (head != null) {
        printListRecursive(head.next); // 先递归到链表尾部
        System.out.print(head.val + " "); // 回溯时打印
    }
}

栈的循环实现

import java.util.Stack;

public void printListLoop(Node head) {
    if (head == null) return;
    Stack<Node> stack = new Stack<>();
    Node cur = head;
    
    // 1. 将链表所有节点压入栈
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    
    // 2. 出栈并打印(逆序)
    while (!stack.empty()) {
        System.out.print(stack.pop().val + " ");
    }
}
3. 其他常见应用
  • 括号匹配:用栈存储左括号,遇到右括号时弹出栈顶左括号,判断是否匹配(如LeetCode 20. 有效的括号)。
  • 逆波兰表达式求值:将中缀表达式转为后缀表达式(逆波兰式),再用栈计算结果(如LeetCode 150. 逆波兰表达式求值)。
  • 最小栈:设计一个栈,能在O(1)时间内获取栈中的最小值(如LeetCode 155. 最小栈)。

1.5 概念辨析:栈、虚拟机栈、栈帧

很多初学者会混淆这三个概念,其实它们分属不同层面:

  • :数据结构层面的概念,是一种“后进先出”的线性表。
  • 虚拟机栈:Java虚拟机(JVM)的内存区域之一,用于存储方法调用时的局部变量、操作数栈等信息。
  • 栈帧:虚拟机栈的基本单位,每个方法调用时会创建一个栈帧,方法执行完后栈帧出栈。

二、队列(Queue):先进先出的“数据管道”

队列类似日常生活中排队买票的场景——先排队的人先买票,后排队的人后买票。这种“先进先出”(FIFO,First In First Out)的特性,是队列的核心标志。

2.1 队列的核心概念

  • 队头(Front):允许删除元素的一端,是队列的“出口”。
  • 队尾(Rear):允许插入元素的一端,是队列的“入口”。
  • 入队(Enqueue):将元素插入队尾的操作。
  • 出队(Dequeue):将队头元素删除并返回的操作。

2.2 队列的常用方法(Java实现)

在Java中,Queue是一个接口(而非类),底层通常通过链表实现,常用实现类是LinkedListQueue的常用方法如下表所示:

方法名功能描述返回值
offer(E e)将元素e入队(队尾)true(成功)/false(失败,如队列满)
poll()移除并返回队头元素队头元素(若队空返回null
peek()获取队头元素(不删除)队头元素(若队空返回null
size()获取队列中有效元素个数整数(元素个数)
isEmpty()判断队列是否为空true(空队)/false(非空)
代码示例:队列的基础使用
import java.util.LinkedList;
import java.util.Queue;

public class QueueDemo {
    public static void main(String[] args) {
        // 1. 初始化队列(Queue是接口,需用LinkedList实例化)
        Queue<Integer> queue = new LinkedList<>();
        
        // 2. 入队操作
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        queue.offer(5);
        System.out.println("队列元素个数:" + queue.size()); // 输出:5
        System.out.println("当前队头元素:" + queue.peek()); // 输出:1
        
        // 3. 出队操作
        queue.poll(); // 1出队
        System.out.println("出队后队头元素:" + queue.poll()); // 输出:2(2出队)
        
        // 4. 判断队列是否为空
        if (queue.isEmpty()) {
            System.out.println("队空");
        } else {
            System.out.println("剩余元素个数:" + queue.size()); // 输出:3(剩余3、4、5)
        }
    }
}

2.3 队列的模拟实现(基于双向链表)

队列的实现有两种选择:顺序结构(数组)和链式结构(链表)。由于数组实现队列时,出队操作会导致数组前端出现“空洞”(需移动元素,时间复杂度O(n)),因此链式结构(尤其是双向链表)是队列的更优选择

核心思路:

  1. 用双向链表存储元素,first指向队头,last指向队尾;
  2. 入队:在队尾(last)添加新节点;
  3. 出队:删除队头(first)节点;
  4. size记录元素个数,或通过first == null判断队列是否为空。
代码实现:自定义双向链表队列
public class MyQueue {
    // 双向链表节点类
    private static class ListNode {
        ListNode prev; // 前驱节点
        ListNode next; // 后继节点
        int value;     // 节点值

        public ListNode(int value) {
            this.value = value;
        }
    }

    private ListNode first; // 队头(出队端)
    private ListNode last;  // 队尾(入队端)
    private int size;       // 元素个数

    // 入队操作(队尾添加)
    public void offer(int e) {
        ListNode newNode = new ListNode(e);
        if (first == null) {
            // 队空:新节点既是队头也是队尾
            first = newNode;
        } else {
            // 队非空:链接到队尾
            last.next = newNode;
            newNode.prev = last;
        }
        last = newNode; // 更新队尾
        size++;
    }

    // 出队操作(队头删除)
    public Integer poll() {
        if (first == null) {
            // 队空:返回null
            return null;
        }
        int value = first.value;
        if (first == last) {
            // 只有一个元素:删除后队空
            first = null;
            last = null;
        } else {
            // 多个元素:删除队头,更新first
            first = first.next;
            first.prev.next = null; // 断开原队头的后继
            first.prev = null;      // 断开新队头的前驱
        }
        size--;
        return value;
    }

    // 获取队头元素
    public Integer peek() {
        if (first == null) {
            return null;
        }
        return first.value;
    }

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

    // 判断队列是否为空
    public boolean isEmpty() {
        return first == null;
    }

    // 测试
    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        queue.offer(10);
        queue.offer(20);
        queue.offer(30);
        System.out.println("队头元素:" + queue.peek()); // 输出:10
        System.out.println("出队元素:" + queue.poll()); // 输出:10
        System.out.println("剩余元素个数:" + queue.size()); // 输出:2
    }
}

2.4 循环队列:解决数组队列的“空洞”问题

用普通数组实现队列时,出队后数组前端会产生“空洞”(如[null,2,3,4]null即为空洞),导致数组空间利用率低。循环队列通过“下标循环”的技巧,让数组空间可重复利用,是操作系统“生产者-消费者模型”的常用数据结构。

1. 循环队列的核心设计
  • 底层存储:数组。
  • 下标循环技巧
    • 下标向后移动:index = (index + offset) % array.length(如index=7array.length=9offset=4,结果为2)。
    • 下标向前移动:index = (index + array.length - offset) % array.length(如index=2array.length=9offset=4,结果为7)。
  • 空队与满队的判断
    • 方法1:用size变量记录元素个数(size=0为空,size=array.length为满)。
    • 方法2:保留一个数组位置(满队时(rear + 1) % array.length == front,空队时front == rear)。
2. 代码实现:循环队列(基于数组+size判断)
public class CircularQueue {
    private int[] array;
    private int front; // 队头下标(出队端)
    private int rear;  // 队尾下标(入队端的下一个位置)
    private int size;  // 元素个数

    // 构造方法:初始化队列容量
    public CircularQueue(int capacity) {
        array = new int[capacity];
        front = 0;
        rear = 0;
        size = 0;
    }

    // 入队
    public boolean offer(int e) {
        if (isFull()) {
            // 队列满,入队失败
            return false;
        }
        array[rear] = e;
        rear = (rear + 1) % array.length; // 循环移动队尾
        size++;
        return true;
    }

    // 出队
    public Integer poll() {
        if (isEmpty()) {
            // 队列空,出队失败
            return null;
        }
        int value = array[front];
        front = (front + 1) % array.length; // 循环移动队头
        size--;
        return value;
    }

    // 获取队头元素
    public Integer peek() {
        if (isEmpty()) {
            return null;
        }
        return array[front];
    }

    // 判断队列是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    // 判断队列是否满
    public boolean isFull() {
        return size == array.length;
    }

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

    // 测试
    public static void main(String[] args) {
        CircularQueue cq = new CircularQueue(3);
        System.out.println(cq.offer(1)); // 输出:true
        System.out.println(cq.offer(2)); // 输出:true
        System.out.println(cq.offer(3)); // 输出:true
        System.out.println(cq.offer(4)); // 输出:false(队列满)
        System.out.println("队头元素:" + cq.peek()); // 输出:1
        System.out.println("出队元素:" + cq.poll()); // 输出:1
        System.out.println("入队元素4:" + cq.offer(4)); // 输出:true(利用循环空间)
    }
}

2.5 双端队列(Deque):灵活的“双向队列”

双端队列(Deque,Double Ended Queue)是队列的扩展,允许在队头和队尾同时进行入队和出队操作,兼具栈和队列的特性。在实际开发中,Deque的使用频率远高于传统的Stack和Queue。

1. Deque的核心特性
  • 可作为栈使用:用push()(队头入队)和pop()(队头出队)模拟栈的操作。
  • 可作为队列使用:用offer()(队尾入队)和poll()(队头出队)模拟队列的操作。
  • Java中,Deque是接口,常用实现类有LinkedList(链式实现)和ArrayDeque(数组实现,效率更高)。
2. 代码示例:Deque的使用
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;

public class DequeDemo {
    public static void main(String[] args) {
        // 1. 作为栈使用(ArrayDeque效率更高)
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(1);
        stack.push(2);
        System.out.println("栈顶元素:" + stack.peek()); // 输出:2
        System.out.println("出栈元素:" + stack.pop()); // 输出:2

        // 2. 作为队列使用(LinkedList实现)
        Deque<Integer> queue = new LinkedList<>();
        queue.offer(10);
        queue.offer(20);
        System.out.println("队头元素:" + queue.peek()); // 输出:10
        System.out.println("出队元素:" + queue.poll()); // 输出:10
    }
}

三、面试实战:栈与队列的相互实现

栈和队列的相互实现是面试中的经典题型,核心是利用“两个栈/队列”模拟对方的操作规则。

3.1 用队列实现栈(LeetCode 225. 用队列实现栈)

思路
  • 用两个队列q1(主队列)和q2(辅助队列);
  • 入栈:直接将元素入队q1
  • 出栈:将q1中除最后一个元素外的所有元素转移到q2,弹出q1的最后一个元素(即栈顶),再将q2的元素转移回q1
代码实现
import java.util.LinkedList;
import java.util.Queue;

class MyStack {
    private Queue<Integer> q1;
    private Queue<Integer> q2;

    public MyStack() {
        q1 = new LinkedList<>();
        q2 = new LinkedList<>();
    }

    // 入栈
    public void push(int x) {
        q1.offer(x);
    }

    // 出栈
    public int pop() {
        // 将q1的n-1个元素转移到q2
        while (q1.size() > 1) {
            q2.offer(q1.poll());
        }
        // 弹出q1的最后一个元素(栈顶)
        int topVal = q1.poll();
        // 交换q1和q2,让q1始终为主队列
        Queue<Integer> temp = q1;
        q1 = q2;
        q2 = temp;
        return topVal;
    }

    // 获取栈顶元素
    public int top() {
        int topVal = pop();
        push(topVal); // 弹出后重新入栈
        return topVal;
    }

    // 判断栈是否为空
    public boolean empty() {
        return q1.isEmpty();
    }
}

3.2 用栈实现队列(LeetCode 232. 用栈实现队列)

思路
  • 用两个栈stackIn(入队栈)和stackOut(出队栈);
  • 入队:直接将元素压入stackIn
  • 出队:若stackOut为空,将stackIn的所有元素转移到stackOut,弹出stackOut的栈顶元素(即队头)。
代码实现
import java.util.Stack;

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() {
        // 若stackOut为空,转移stackIn的元素
        if (stackOut.empty()) {
            while (!stackIn.empty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.pop();
    }

    // 获取队头元素
    public int peek() {
        int frontVal = pop();
        stackOut.push(frontVal); // 弹出后重新压栈
        return frontVal;
    }

    // 判断队列是否为空
    public boolean empty() {
        return stackIn.empty() && stackOut.empty();
    }
}

四、总结

栈和队列是数据结构的基石,它们的核心区别在于元素操作规则:

  • :后进先出 ,仅栈顶可操作,适合需要“逆序处理”的场景(如递归转循环、括号匹配)。
  • 队列:先进先出 ,队头出、队尾入,适合需要“顺序处理”的场景(如任务调度、生产者-消费者模型)。

掌握栈和队列的实现原理与应用场景,不仅能应对面试中的基础题型,更能为后续复杂算法(如动态规划、图论)的学习打下坚实基础。建议大家结合本文代码,动手实现一遍,加深对这两种结构的理解!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值