JAVA数据结构 DAY6-栈和队列

本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。

点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!

系列文章目录

JAVA初阶---------已更完

JAVA数据结构 DAY1-集合和时空复杂度

JAVA数据结构 DAY2-包装类和泛型

JAVA数据结构 DAY3-List接口

JAVA数据结构 DAY4-ArrayList

JAVA数据结构 DAY5-LinkedList

JAVA数据结构 DAY6-栈和队列​​​​​​​


拓展目录

手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解


目录

目录

系列文章目录

拓展目录

目录

前言

[一、栈(Stack):后进先出的 "有序容器"](#一、栈(Stack):后进先出的 “有序容器”)

[1.1 概念深度剖析](#1.1 概念深度剖析)

核心术语详解

现实场景类比

[1.2 栈的常用方法与实战示例](#1.2 栈的常用方法与实战示例)

核心方法详解

实战代码与易错点分析

[1.3 栈的模拟实现(基于数组,超详细注释)](#1.3 栈的模拟实现(基于数组,超详细注释))

模拟实现关键细节

[1.4 栈的核心应用场景(含真题解析)](#1.4 栈的核心应用场景(含真题解析))

[场景 1:判断出栈序列的合法性](#场景 1:判断出栈序列的合法性)

[场景 2:递归转循环(以逆序打印链表为例)](#场景 2:递归转循环(以逆序打印链表为例))

[场景 3:括号匹配(LeetCode 经典题)](#场景 3:括号匹配(LeetCode 经典题))

[场景 4:其他核心应用](#场景 4:其他核心应用)

[1.5 易混淆概念区分(栈、虚拟机栈、栈帧)](#1.5 易混淆概念区分(栈、虚拟机栈、栈帧))

[二、队列(Queue):先进先出的 "有序队列"](#二、队列(Queue):先进先出的 “有序队列”)

[2.1 概念深度剖析](#2.1 概念深度剖析)

核心术语详解

现实场景类比

[2.2 队列的常用方法与实战示例](#2.2 队列的常用方法与实战示例)

核心方法详解

实战代码与注意事项

[2.3 队列的模拟实现(基于双向链表,超详细)](#2.3 队列的模拟实现(基于双向链表,超详细))

模拟实现关键细节

[2.4 循环队列(数组实现,解决假溢出问题)](#2.4 循环队列(数组实现,解决假溢出问题))

[2.4.1 为什么需要循环队列?](#2.4.1 为什么需要循环队列?)

[2.4.2 循环队列的核心设计](#2.4.2 循环队列的核心设计)

[2.4.3 循环队列的模拟实现(基于数组,保留空闲位置)](#2.4.3 循环队列的模拟实现(基于数组,保留空闲位置))

循环队列关键细节

[2.5 循环队列的应用场景](#2.5 循环队列的应用场景)

[三、双端队列(Deque):兼具栈与队列特性的 "全能选手"](#三、双端队列(Deque):兼具栈与队列特性的 “全能选手”)

[3.1 概念深度剖析](#3.1 概念深度剖析)

核心特性

[3.2 Deque 的实现类与使用示例](#3.2 Deque 的实现类与使用示例)

常用方法(核心)

[实战示例:用 Deque 模拟栈和队列](#实战示例:用 Deque 模拟栈和队列)

[3.3 Deque 的优势与应用场景](#3.3 Deque 的优势与应用场景)

四、面试高频真题与解题思路

[真题 1:用栈实现队列(LeetCode 232)](#真题 1:用栈实现队列(LeetCode 232))

[真题 2:用队列实现栈(LeetCode 225)](#真题 2:用队列实现栈(LeetCode 225))

五、总结

总结


前言

小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

在数据结构与算法中,栈和队列是两种基础且核心的线性表变体,它们通过严格的操作规则约束元素的插入与删除,广泛应用于编程开发、算法设计、操作系统等领域。本文将从概念细节、方法拆解、模拟实现、应用场景、面试真题等维度,进行全方位、超详细的解析,帮助读者彻底掌握栈与队列的核心知识。

一、栈(Stack):后进先出的 "有序容器"

1.1 概念深度剖析

栈是一种操作受限的线性表 ,仅允许在表的一端(栈顶)进行插入和删除操作,另一端(栈底)固定不变。其核心特性是后进先出(LIFO,Last In First Out),即最后插入的元素最先被删除,就像日常生活中叠放的书籍 ------ 最后放上的书,能最先被拿到。

核心术语详解
  • 栈顶(Top):允许插入和删除操作的一端,栈中元素的 "活跃端",元素入栈和出栈均围绕栈顶进行;
  • 栈底(Bottom):固定不变的一端,是栈中最早插入元素的存放位置;
  • 压栈 / 入栈(Push):将元素插入栈顶的操作,插入后该元素成为新的栈顶;
  • 出栈(Pop):将栈顶元素删除并返回的操作,删除后栈顶指针向下移动(指向原栈顶的下一个元素);
  • 空栈:栈中无任何元素的状态,此时栈顶与栈底重合。
现实场景类比
  • 叠放的盘子:只能从最上方拿取盘子,也只能在最上方添加盘子;
  • 浏览器的历史记录:点击 "后退" 按钮时,最后访问的页面最先被返回;
  • 函数调用栈:JVM 中,函数调用时会创建栈帧压入虚拟机栈,函数执行完毕后栈帧出栈,遵循 "后调用先执行完毕" 的规则。

1.2 栈的常用方法与实战示例

Java 中提供了java.util.Stack类实现栈结构,该类继承自Vector(线程安全的动态数组),提供了完整的栈操作方法。以下是方法的详细说明及实战代码(含易错点解析):

核心方法详解
方法 功能描述 注意事项
Stack() 构造一个空栈 无参数,初始化时栈的容量由Vector默认规则决定(初始容量 10,扩容时翻倍)
E push(E e) 将元素e压入栈顶,返回被压入的元素e 入栈成功后,栈的有效元素个数(size)加 1
E pop() 移除并返回栈顶元素 若栈为空,会抛出EmptyStackException,需提前用empty()判断
E peek() 获取栈顶元素(不删除) 仅查看栈顶,不改变栈的结构;栈为空时抛出EmptyStackException
int size() 返回栈中有效元素的个数 取值范围为0(空栈)到栈的最大容量
boolean empty() 判断栈是否为空,为空返回true,否则返回false 等同于size() == 0,但效率更高(直接判断内部计数器)
实战代码与易错点分析
java 复制代码
import java.util.Stack;

public class StackDemo {
    public static void main(String[] args) {
        // 1. 构造空栈
        Stack<Integer> stack = new Stack<>();
        
        // 2. 入栈操作(push)
        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.push(4);
        System.out.println("入栈后栈的元素(从栈底到栈顶):1,2,3,4");
        
        // 3. 获取栈顶元素(peek)
        Integer top = stack.peek();
        System.out.println("当前栈顶元素:" + top); // 输出4(正确,栈顶为最后入栈的元素)
        
        // 4. 获取有效元素个数(size)
        System.out.println("栈中元素个数:" + stack.size()); // 输出4(4个元素均未出栈)
        
        // 5. 出栈操作(pop)
        Integer pop1 = stack.pop();
        System.out.println("第一次出栈元素:" + pop1); // 输出4(栈顶元素出栈)
        System.out.println("出栈后栈顶元素:" + stack.peek()); // 输出3(新栈顶为3)
        System.out.println("出栈后元素个数:" + stack.size()); // 输出3(个数减1)
        
        // 6. 空栈判断(empty)
        if (stack.empty()) {
            System.out.println("栈为空");
        } else {
            System.out.println("栈非空,剩余元素:1,2,3");
        }
        
        // 易错点1:栈为空时调用pop()/peek()会抛异常
        // 连续出栈3次,使栈为空
        stack.pop(); // 出栈3
        stack.pop(); // 出栈2
        stack.pop(); // 出栈1
        System.out.println("连续出栈后栈是否为空:" + stack.empty()); // 输出true
        // stack.pop(); // 报错:EmptyStackException,需避免
        
        // 易错点2:push()方法的返回值是被压入的元素,而非栈顶元素(虽结果一致,但语义不同)
        Integer pushResult = stack.push(5);
        System.out.println("push(5)的返回值:" + pushResult); // 输出5(正确)
    }
}

1.3 栈的模拟实现(基于数组,超详细注释)

为深入理解栈的底层原理,我们基于动态数组 手动实现栈(复刻Stack类的核心逻辑,补充完整边界处理):

java 复制代码
import java.util.Arrays;

public class MyStack {
    // 存储栈元素的数组(底层容器)
    private int[] array;
    // 记录栈中有效元素的个数(栈顶指针的本质:size-1即为栈顶元素的下标)
    private int size;
    // 栈的初始容量(可自定义,此处设为3)
    private static final int INIT_CAPACITY = 3;

    // 1. 构造函数:初始化空栈
    public MyStack() {
        this.array = new int[INIT_CAPACITY];
        this.size = 0; // 初始时无有效元素
    }

    // 2. 入栈操作:将元素压入栈顶
    public int push(int e) {
        // 先确保数组容量充足,不足则扩容
        ensureCapacity();
        // 将元素存入栈顶(size为当前有效元素个数,下标为size的位置是新栈顶)
        array[size] = e;
        size++; // 有效元素个数加1
        return e; // 返回被压入的元素
    }

    // 3. 出栈操作:移除并返回栈顶元素
    public int pop() {
        // 先判断栈是否为空,空栈直接抛异常(符合Java规范)
        if (empty()) {
            throw new RuntimeException("栈为空,无法执行出栈操作(EmptyStackException)");
        }
        // 获取栈顶元素(下标为size-1)
        int topElement = array[size - 1];
        size--; // 有效元素个数减1(间接实现栈顶指针下移,无需真正删除元素)
        return topElement;
    }

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

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

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

    // 核心辅助方法:确保数组容量充足,不足则扩容(扩容规则:容量翻倍)
    private void ensureCapacity() {
        // 当有效元素个数等于数组长度时,说明数组已满,需要扩容
        if (size == array.length) {
            // 扩容:将原数组复制到新数组,新数组长度为原长度的2倍
            int newCapacity = array.length * 2;
            array = Arrays.copyOf(array, newCapacity);
            System.out.println("栈扩容成功:原容量" + array.length/2 + " → 新容量" + newCapacity);
        }
    }

    // 测试方法
    public static void main(String[] args) {
        MyStack myStack = new MyStack();
        myStack.push(1);
        myStack.push(2);
        myStack.push(3);
        System.out.println("当前栈顶元素:" + myStack.peek()); // 输出3
        myStack.push(4); // 触发扩容:原容量3→6
        System.out.println("入栈4后栈顶元素:" + myStack.peek()); // 输出4
        System.out.println("出栈元素:" + myStack.pop()); // 输出4
        System.out.println("出栈后栈顶元素:" + myStack.peek()); // 输出3
        System.out.println("栈中元素个数:" + myStack.size()); // 输出3
    }
}
模拟实现关键细节
  • 栈顶指针的实现 :通过size变量间接表示栈顶 ------size-1是栈顶元素的下标,无需额外定义指针变量;
  • 扩容机制 :当数组满时(size == array.length),通过Arrays.copyOf将容量翻倍,保证入栈操作的连续性;
  • 边界处理 :空栈时调用pop()peek()会抛出明确异常,与 Java 原生Stack类行为一致,增强鲁棒性。

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

解析步骤

  1. 遍历出栈序列,对每个元素执行 "入栈到目标元素,然后出栈" 的操作;
  2. 若过程中栈顶元素与出栈序列当前元素不一致,且无剩余元素可入栈,则序列非法。

选项 C 分析

  • 出栈序列第一个元素为 3:需先入栈 1、2、3,然后出栈 3(栈中剩余 1、2,栈顶为 2);
  • 出栈序列第二个元素为 1:此时栈顶为 2,不等于 1,且无更多元素可入栈(入栈序列仅剩余 4),无法得到 1,因此序列非法。

答案:C

真题 2 :一个栈的初始状态为空,现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )A: 12345ABCDE B: EDCBA54321 C: ABCDE12345 D: 54321EDCBA

解析 :依次入栈后,栈中元素从栈底到栈顶为1、2、3、4、5、A、B、C、D、E,依次出栈遵循 LIFO 原则,最后入栈的 E 最先出栈,依次类推,最终顺序为EDCBA54321

答案:B

场景 2:递归转循环(以逆序打印链表为例)

递归的底层实现依赖虚拟机栈,当递归深度过大时可能导致栈溢出,此时可通过栈将递归转化为循环。

递归实现

java 复制代码
// 递归逆序打印链表(依赖虚拟机栈保存方法调用)
public void printListReverse(Node head) {
    if (head != null) {
        printListReverse(head.next); // 先递归打印后续节点
        System.out.print(head.val + " "); // 最后打印当前节点
    }
}

栈实现循环(无递归)

java 复制代码
// 用栈逆序打印链表(手动模拟递归栈)
public void printListReverseWithStack(Node head) {
    if (head == null) {
        return;
    }
    Stack<Node> stack = new Stack<>();
    // 第一步:将链表所有节点入栈(从表头到表尾)
    Node cur = head;
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    // 第二步:出栈并打印(栈顶为链表尾节点,实现逆序)
    while (!stack.empty()) {
        System.out.print(stack.pop().val + " ");
    }
}

// 链表节点定义
class Node {
    int val;
    Node next;
    Node(int val) {
        this.val = val;
    }
}

核心逻辑:通过栈存储链表节点,入栈顺序为 "表头→表尾",出栈顺序为 "表尾→表头",完美复刻递归的逆序效果,且避免栈溢出风险。

场景 3:括号匹配(LeetCode 经典题)

问题描述 :给定一个只包含'('、')'、'{'、'}'、'['、']'的字符串,判断字符串是否有效(有效条件:括号必须成对出现,且左括号必须与对应的右括号匹配)。

解题思路

  1. 遍历字符串,遇到左括号((、{、[)则压入栈;
  2. 遇到右括号()、}、]),判断栈是否为空(为空则无匹配的左括号,无效);
  3. 若栈非空,弹出栈顶元素,判断是否为对应的左括号(如)对应(),不匹配则无效;
  4. 遍历结束后,栈必须为空(否则存在未匹配的左括号,无效)。

代码实现

java 复制代码
import java.util.Stack;

public class ValidParentheses {
    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.empty()) {
                    return false;
                }
                // 弹出栈顶左括号,判断是否匹配
                char top = stack.pop();
                if ((c == ')' && top != '(') || 
                    (c == '}' && top != '{') || 
                    (c == ']' && top != '[')) {
                    return false;
                }
            }
        }
        // 遍历结束后,栈必须为空(无未匹配的左括号)
        return stack.empty();
    }

    public static void main(String[] args) {
        ValidParentheses vp = new ValidParentheses();
        System.out.println(vp.isValid("()[]{}")); // true
        System.out.println(vp.isValid("(]")); // false
        System.out.println(vp.isValid("([)]")); // false
        System.out.println(vp.isValid("{[]}")); // true
    }
}
场景 4:其他核心应用
  • 逆波兰表达式求值:逆波兰表达式(后缀表达式)是 "操作数 + 操作数 + 运算符" 的形式,通过栈存储操作数,遇到运算符时弹出两个操作数计算,结果再入栈;
  • 最小栈 :设计一个支持pushpoptop操作,并能在常数时间内检索到最小元素的栈(解法:维护两个栈,一个存储元素,一个存储当前最小值);
  • 出栈入栈次序匹配:判断一组入栈和出栈操作序列是否合法(核心:出栈时栈中必须有对应元素)。

1.5 易混淆概念区分(栈、虚拟机栈、栈帧)

很多初学者会混淆 "栈" 的三个相关概念,这里进行明确区分:

概念 本质 归属 核心作用
栈(数据结构) 操作受限的线性表 数据结构范畴 存储数据,遵循 LIFO 原则,用于序列处理、匹配等场景
虚拟机栈(JVM 内存区域) JVM 运行时数据区域之一 JVM 内存模型 存储方法调用时的栈帧,用于管理方法的执行与返回
栈帧(Stack Frame) 方法调用时创建的内存块 虚拟机栈的组成单元 每个栈帧包含局部变量表、操作数栈、方法返回地址等,方法调用时入栈,执行完毕后出栈

通俗理解:虚拟机栈是 "容器",栈帧是 "容器中的物品",而数据结构中的栈是 "物品的存储规则"------ 虚拟机栈通过栈帧的 "入栈 / 出栈"(遵循 LIFO)来实现方法调用,本质是数据结构栈的实际应用。

二、队列(Queue):先进先出的 "有序队列"

2.1 概念深度剖析

队列是另一种操作受限的线性表 ,仅允许在表的一端(队尾)进行插入操作,在另一端(队头)进行删除操作,核心特性是先进先出(FIFO,First In First Out),即最早插入的元素最先被删除,就像日常生活中排队买票 ------ 先排队的人先购票,后排队的人只能在队尾等待。

核心术语详解
  • 队头(Head/Front):允许删除操作的一端,是队列中最早插入元素的存放位置;
  • 队尾(Tail/Rear):允许插入操作的一端,是队列中最新插入元素的存放位置;
  • 入队(Enqueue):将元素插入队尾的操作,插入后该元素成为新的队尾;
  • 出队(Dequeue):将队头元素删除并返回的操作,删除后队头指针向后移动(指向原队头的下一个元素);
  • 空队列:队列中无任何元素的状态,此时队头与队尾重合。
现实场景类比
  • 排队买票 / 结账:先到先服务,队尾新增排队人员,队头完成服务后离开;
  • 打印机队列:多个文档请求打印时,按请求顺序依次打印;
  • 消息队列:分布式系统中,消息生产者将消息入队,消费者从队头获取消息,实现异步通信。

2.2 队列的常用方法与实战示例

Java 中Queue是一个接口(而非类),位于java.util包下,其底层通常通过链表实现(因为链表的头尾操作效率更高)。由于接口无法直接实例化,需使用实现类(如LinkedListArrayDeque)来创建队列对象。

核心方法详解
方法 功能描述 注意事项
boolean offer(E e) 将元素e入队(添加到队尾),成功返回true,失败返回false add(E e)的区别:add失败时抛出异常,offer返回false(推荐使用offer
E poll() 移除并返回队头元素,队列为空时返回null remove()的区别:remove为空时抛出异常,poll返回null(推荐使用poll
E peek() 获取队头元素(不删除),队列为空时返回null element()的区别:element为空时抛出异常,peek返回null(推荐使用peek
int size() 返回队列中有效元素的个数 取值范围为0(空队列)到队列的最大容量
boolean isEmpty() 判断队列是否为空,为空返回true,否则返回false 等同于size() == 0,效率更高
实战代码与注意事项
java 复制代码
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. 入队操作(offer)
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        queue.offer(5);
        System.out.println("入队后队列元素(从队头到队尾):1,2,3,4,5");
        
        // 3. 获取队头元素(peek)
        Integer head = queue.peek();
        System.out.println("当前队头元素:" + head); // 输出1(队头为最早入队的元素)
        
        // 4. 获取有效元素个数(size)
        System.out.println("队列中元素个数:" + queue.size()); // 输出5
        
        // 5. 出队操作(poll)
        Integer poll1 = queue.poll();
        System.out.println("第一次出队元素:" + poll1); // 输出1(队头元素出队)
        System.out.println("出队后队头元素:" + queue.peek()); // 输出2(新队头为2)
        System.out.println("出队后元素个数:" + queue.size()); // 输出4
        
        // 6. 空队列判断(isEmpty)
        if (queue.isEmpty()) {
            System.out.println("队列为空");
        } else {
            System.out.println("队列非空,剩余元素:2,3,4,5");
        }
        
        // 注意事项1:Queue接口不能用Stack实现类实例化(Stack未实现Queue接口)
        // Queue<Integer> wrongQueue = new Stack<>(); // 编译报错
        
        // 注意事项2:poll()和peek()在空队列时返回null,而非抛出异常
        // 连续出队4次,使队列空
        queue.poll(); // 出队2
        queue.poll(); // 出队3
        queue.poll(); // 出队4
        queue.poll(); // 出队5
        System.out.println("连续出队后队列是否为空:" + queue.isEmpty()); // 输出true
        System.out.println("空队列poll()返回值:" + queue.poll()); // 输出null
        System.out.println("空队列peek()返回值:" + queue.peek()); // 输出null
    }
}

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

队列的底层实现有两种选择:顺序结构(数组)和链式结构(链表)。由于数组实现队列时,出队操作(删除队头元素)需要移动后续所有元素,时间复杂度为 O (n),而链表实现的出队和入队操作时间复杂度均为 O (1),因此推荐使用链表实现队列

以下是基于双向链表的队列模拟实现(含完整边界处理和测试):

java 复制代码
public class MyQueue {
    // 双向链表节点类(存储队列元素)
    private static class ListNode {
        int value; // 节点值
        ListNode prev; // 前驱节点指针(用于双向链表反向遍历)
        ListNode next; // 后继节点指针

        // 节点构造函数
        public ListNode(int value) {
            this.value = value;
            this.prev = null;
            this.next = null;
        }
    }

    private ListNode first; // 队头指针(指向双向链表的第一个节点)
    private ListNode last; // 队尾指针(指向双向链表的最后一个节点)
    private int size; // 队列中有效元素的个数

    // 1. 构造函数:初始化空队列
    public MyQueue() {
        this.first = null;
        this.last = null;
        this.size = 0;
    }

    // 2. 入队操作:将元素添加到队尾(双向链表的尾部插入)
    public void offer(int e) {
        ListNode newNode = new ListNode(e); // 创建新节点
        if (first == null) { // 队列空时,新节点既是队头也是队尾
            first = newNode;
            last = newNode;
        } else { // 队列非空时,将新节点链接到队尾
            last.next = newNode; // 原队尾的next指向新节点
            newNode.prev = last; // 新节点的prev指向原队尾
            last = newNode; // 更新队尾指针为新节点
        }
        size++; // 有效元素个数加1
    }

    // 3. 出队操作:移除并返回队头元素(双向链表的头部删除)
    public Integer poll() {
        // 情况1:队列为空,返回null
        if (first == null) {
            return null;
        }
        // 保存队头元素的值(用于返回)
        int headValue = first.value;
        // 情况2:队列中只有一个元素(队头和队尾指向同一个节点)
        if (first == last) {
            first = null;
            last = null;
        } else { // 情况3:队列中有多个元素
            first = first.next; // 队头指针指向原队头的下一个节点
            first.prev.next = null; // 断开原队头的next链接(垃圾回收)
            first.prev = null; // 新队头的prev置为null(避免循环引用)
        }
        size--; // 有效元素个数减1
        return headValue;
    }

    // 4. 获取队头元素(不删除)
    public Integer peek() {
        // 队列为空时返回null
        return first == null ? null : first.value;
    }

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

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

    // 测试方法
    public static void main(String[] args) {
        MyQueue myQueue = new MyQueue();
        myQueue.offer(1);
        myQueue.offer(2);
        myQueue.offer(3);
        System.out.println("当前队头元素:" + myQueue.peek()); // 输出1
        System.out.println("队列元素个数:" + myQueue.size()); // 输出3
        
        System.out.println("出队元素:" + myQueue.poll()); // 输出1
        System.out.println("出队后队头元素:" + myQueue.peek()); // 输出2
        System.out.println("出队后元素个数:" + myQueue.size()); // 输出2
        
        myQueue.offer(4);
        System.out.println("入队4后队头元素:" + myQueue.peek()); // 输出2
        System.out.println("入队4后元素个数:" + myQueue.size()); // 输出3
        
        // 清空队列
        myQueue.poll(); // 出队2
        myQueue.poll(); // 出队3
        myQueue.poll(); // 出队4
        System.out.println("清空后队列是否为空:" + myQueue.isEmpty()); // 输出true
        System.out.println("空队列poll()返回值:" + myQueue.poll()); // 输出null
    }
}
模拟实现关键细节
  • 双向链表的优势:相比单向链表,双向链表的头部删除操作更高效(无需遍历找到前驱节点),仅需调整指针即可;
  • 队头 / 队尾指针 :通过firstlast指针直接指向队头和队尾节点,入队和出队操作均为 O (1) 时间复杂度;
  • 边界处理 :队列为空时poll()peek()返回null,队列只有一个元素时需同时更新firstlast指针,避免空指针异常。

2.4 循环队列(数组实现,解决假溢出问题)

2.4.1 为什么需要循环队列?

普通数组实现的队列存在 "假溢出" 问题:当队尾指针达到数组末尾时,队列看似已满,但数组前端可能存在空闲空间(因元素出队后前端位置空出)。循环队列通过 "下标循环复用" 的方式,解决了假溢出问题,提高了数组空间的利用率。

循环队列的核心思想:将数组视为一个环形空间,队尾指针到达数组末尾时,若数组前端有空闲空间,队尾指针可绕回数组开头继续入队。

2.4.2 循环队列的核心设计
  • 底层存储:数组(固定容量,需提前指定);
  • 队头指针(front):指向队头元素的下标;
  • 队尾指针(rear):指向队尾元素的下一个空闲位置(便于计算入队位置);
  • 下标循环技巧
    1. 下标向后移动(入队时 rear 更新):rear = (rear + 1) % array.length
    2. 下标向前移动(出队时 front 更新):front = (front + 1) % array.length
  • 空满判断(关键难点) :由于front == rear既可能表示队列空,也可能表示队列满,需通过以下方式区分:
    1. 方法 1:保留一个空闲位置 :队列满时,(rear + 1) % array.length == front;队列空时,front == rear(推荐使用,实现简单);
    2. 方法 2:维护 size 变量size == 0表示空,size == array.length表示满;
    3. 方法 3:使用标记位 :新增一个flag变量,flag == false表示空,flag == true表示满。
2.4.3 循环队列的模拟实现(基于数组,保留空闲位置)
java 复制代码
public class CircularQueue {
    private int[] array; // 存储元素的数组
    private int front; // 队头指针(指向队头元素)
    private int rear; // 队尾指针(指向队尾元素的下一个空闲位置)
    private int capacity; // 队列的最大容量(数组长度)

    // 构造函数:指定队列最大容量
    public CircularQueue(int capacity) {
        this.capacity = capacity;
        this.array = new int[capacity];
        this.front = 0; // 初始时队头指针为0
        this.rear = 0; // 初始时队尾指针为0(队列空)
    }

    // 1. 入队操作:将元素添加到队尾
    public boolean offer(int e) {
        // 先判断队列是否满
        if (isFull()) {
            System.out.println("队列已满,入队失败");
            return false;
        }
        // 将元素存入rear指向的空闲位置
        array[rear] = e;
        // 队尾指针循环后移
        rear = (rear + 1) % capacity;
        return true;
    }

    // 2. 出队操作:移除并返回队头元素
    public Integer poll() {
        // 先判断队列是否空
        if (isEmpty()) {
            System.out.println("队列为空,出队失败");
            return null;
        }
        // 保存队头元素的值
        int headValue = array[front];
        // 队头指针循环后移
        front = (front + 1) % capacity;
        return headValue;
    }

    // 3. 获取队头元素(不删除)
    public Integer peek() {
        if (isEmpty()) {
            System.out.println("队列为空,无法获取队头元素");
            return null;
        }
        return array[front];
    }

    // 4. 判断队列是否满(核心:(rear + 1) % capacity == front)
    public boolean isFull() {
        return (rear + 1) % capacity == front;
    }

    // 5. 判断队列是否空(front == rear)
    public boolean isEmpty() {
        return front == rear;
    }

    // 6. 获取队列中有效元素个数
    public int size() {
        return (rear - front + capacity) % capacity;
    }

    // 测试方法
    public static void main(String[] args) {
        CircularQueue cq = new CircularQueue(5); // 最大容量5(实际可存储4个元素,保留1个空闲位置)
        cq.offer(1);
        cq.offer(2);
        cq.offer(3);
        cq.offer(4);
        System.out.println("队列是否满:" + cq.isFull()); // 输出true(4个元素已存满)
        cq.offer(5); // 入队失败(队列满)
        
        System.out.println("队头元素:" + cq.peek()); // 输出1
        System.out.println("有效元素个数:" + cq.size()); // 输出4
        
        System.out.println("出队元素:" + cq.poll()); // 输出1
        System.out.println("出队后队列是否满:" + cq.isFull()); // 输出false
        cq.offer(5); // 入队成功
        System.out.println("入队5后队头元素:" + cq.peek()); // 输出2
        System.out.println("有效元素个数:" + cq.size()); // 输出4
    }
}
循环队列关键细节
  • 容量说明 :若数组长度为capacity,采用 "保留一个空闲位置" 的判断方式时,队列的最大存储元素个数为capacity - 1(如数组长度 5,最多存 4 个元素);
  • 下标循环 :通过取模运算(% capacity)实现下标绕回,避免数组越界;
  • 元素个数计算(rear - front + capacity) % capacity,确保在rear < front(队尾绕回数组开头)时也能正确计算个数。

2.5 循环队列的应用场景

  • 生产者 - 消费者模型:操作系统中,生产者线程向队列中添加数据,消费者线程从队列中获取数据,循环队列的空间复用特性适合高并发场景;
  • 缓冲区设计:如 IO 缓冲区、网络数据缓冲区,通过循环队列暂存数据,避免频繁扩容;
  • 滑动窗口算法:部分滑动窗口问题可通过循环队列优化,提高算法效率。

三、双端队列(Deque):兼具栈与队列特性的 "全能选手"

3.1 概念深度剖析

双端队列(Deque,全称 Double Ended Queue)是一种操作不受限的队列 ,允许在队头和队尾两端同时进行入队和出队操作。它融合了栈和队列的特性:

  • 若仅使用队头的入队和出队操作,可模拟栈(LIFO);
  • 若仅使用队尾入队、队头出队操作,可模拟普通队列(FIFO)。
核心特性
  • 两端均可插入元素(addFirst/offerFirstaddLast/offerLast);
  • 两端均可删除元素(removeFirst/pollFirstremoveLast/pollLast);
  • 可获取两端元素(getFirst/peekFirstgetLast/peekLast)。

3.2 Deque 的实现类与使用示例

Java 中Deque是接口,继承自Queue接口,常用实现类有:

  • LinkedList:基于双向链表实现,支持高效的两端操作,适合元素个数不固定的场景;
  • ArrayDeque:基于数组实现,两端操作效率高(比LinkedList更快),适合元素个数固定或变化不大的场景;
  • LinkedBlockingDeque:线程安全的双端队列,适合并发场景。
常用方法(核心)
方法 功能
void addFirst(E e) 在队头插入元素,失败抛出异常
void addLast(E e) 在队尾插入元素,失败抛出异常
boolean offerFirst(E e) 在队头插入元素,失败返回false
boolean offerLast(E e) 在队尾插入元素,失败返回false
E removeFirst() 删除并返回队头元素,空队列抛出异常
E removeLast() 删除并返回队尾元素,空队列抛出异常
E pollFirst() 删除并返回队头元素,空队列返回null
E pollLast() 删除并返回队尾元素,空队列返回null
E getFirst() 获取队头元素,空队列抛出异常
E getLast() 获取队尾元素,空队列抛出异常
E peekFirst() 获取队头元素,空队列返回null
E peekLast() 获取队尾元素,空队列返回null
实战示例:用 Deque 模拟栈和队列
java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

public class DequeDemo {
    public static void main(String[] args) {
        // 1. 用Deque模拟栈(LIFO):仅使用队头的入队和出队操作
        Deque<Integer> stack = new ArrayDeque<>();
        stack.offerFirst(1); // 队头入队(模拟push)
        stack.offerFirst(2);
        stack.offerFirst(3);
        System.out.println("栈顶元素(peekFirst):" + stack.peekFirst()); // 输出3
        System.out.println("出栈元素(pollFirst):" + stack.pollFirst()); // 输出3(模拟pop)
        System.out.println("栈中剩余元素个数:" + stack.size()); // 输出2
        
        // 2. 用Deque模拟普通队列(FIFO):队尾入队,队头出队
        Deque<Integer> queue = new ArrayDeque<>();
        queue.offerLast(1); // 队尾入队(模拟offer)
        queue.offerLast(2);
        queue.offerLast(3);
        System.out.println("队头元素(peekFirst):" + queue.peekFirst()); // 输出1
        System.out.println("出队元素(pollFirst):" + queue.pollFirst()); // 输出1(模拟poll)
        System.out.println("队列中剩余元素个数:" + queue.size()); // 输出2
        
        // 3. 双端操作示例
        Deque<String> deque = new ArrayDeque<>();
        deque.offerFirst("A"); // 队头入队:[A]
        deque.offerLast("B"); // 队尾入队:[A, B]
        deque.offerFirst("C"); // 队头入队:[C, A, B]
        System.out.println("队头元素:" + deque.peekFirst()); // 输出C
        System.out.println("队尾元素:" + deque.peekLast()); // 输出B
        System.out.println("删除队尾元素:" + deque.pollLast()); // 输出B
        System.out.println("删除后队列:[C, A]");
    }
}

3.3 Deque 的优势与应用场景

  • 替代 Stack 类 :Java 原生Stack类继承自Vector,存在线程安全开销,且方法命名不够直观,DequeofferFirst/pollFirst可更高效地模拟栈操作;
  • 替代 Queue 接口Deque兼容Queue的所有方法(如offer等价于offerLastpoll等价于pollFirst),且提供更多双端操作;
  • 实际应用:滑动窗口最大值(LeetCode 239)、单调队列、表达式求值等算法题中,Deque 是核心数据结构。

四、面试高频真题与解题思路

真题 1:用栈实现队列(LeetCode 232)

题目描述 :请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(pushpoppeekempty)。

解题思路

  • 两个栈分工:pushStack负责入队(存储元素),popStack负责出队(输出元素);
  • 入队操作:直接将元素压入pushStack
  • 出队 /peek 操作:若popStack为空,将pushStack中所有元素弹出并压入popStack(此时popStack的栈顶为队列的队头),再执行出队或 peek。

代码实现

java 复制代码
import java.util.Stack;

class MyQueue {
    private Stack<Integer> pushStack;
    private Stack<Integer> popStack;

    public MyQueue() {
        pushStack = new Stack<>();
        popStack = new Stack<>();
    }

    // 入队
    public void push(int x) {
        pushStack.push(x);
    }

    // 出队
    public int pop() {
        if (popStack.empty()) {
            // 将pushStack的元素转移到popStack
            while (!pushStack.empty()) {
                popStack.push(pushStack.pop());
            }
        }
        return popStack.pop();
    }

    // 获取队头元素
    public int peek() {
        if (popStack.empty()) {
            while (!pushStack.empty()) {
                popStack.push(pushStack.pop());
            }
        }
        return popStack.peek();
    }

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

真题 2:用队列实现栈(LeetCode 225)

题目描述 :请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushpoptopempty)。

解题思路

  • 两个队列分工:activeQueue(活跃队列,存储当前元素)和tempQueue(临时队列,辅助出栈);
  • 入队操作:直接将元素加入activeQueue
  • 出栈 /top 操作:将activeQueue中前size-1个元素转移到tempQueue,此时activeQueue中剩余的 1 个元素即为栈顶,执行出栈或 top 后,交换activeQueuetempQueue的引用。

代码实现

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

class MyStack {
    private Queue<Integer> activeQueue;
    private Queue<Integer> tempQueue;

    public MyStack() {
        activeQueue = new LinkedList<>();
        tempQueue = new LinkedList<>();
    }

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

    // 出栈
    public int pop() {
        // 将activeQueue中前size-1个元素转移到tempQueue
        while (activeQueue.size() > 1) {
            tempQueue.offer(activeQueue.poll());
        }
        // activeQueue中剩余的1个元素即为栈顶
        int top = activeQueue.poll();
        // 交换activeQueue和tempQueue的引用
        Queue<Integer> temp = activeQueue;
        activeQueue = tempQueue;
        tempQueue = temp;
        return top;
    }

    // 获取栈顶元素
    public int top() {
        while (activeQueue.size() > 1) {
            tempQueue.offer(activeQueue.poll());
        }
        int top = activeQueue.peek();
        // 注意:top操作不删除元素,需将栈顶元素转移到tempQueue
        tempQueue.offer(activeQueue.poll());
        Queue<Integer> temp = activeQueue;
        activeQueue = tempQueue;
        tempQueue = temp;
        return top;
    }

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

五、总结

栈和队列是两种基础且核心的数据结构,其核心区别在于操作规则:

  • 栈:后进先出(LIFO),仅允许栈顶操作,适合逆序处理、匹配、递归转循环等场景;
  • 队列:先进先出(FIFO),仅允许队尾入队、队头出队,适合顺序处理、缓冲、异步通信等场景;
  • 双端队列(Deque):融合两者特性,两端均可操作,是实际开发中的优选(替代StackQueue)。

掌握栈和队列的关键在于:

  1. 理解其 "操作受限" 的本质,以及 LIFO/FIFO 特性在实际场景中的应用;
  2. 熟练掌握模拟实现(数组 / 链表),理解底层原理;
  3. 攻克面试高频题(用栈实现队列、用队列实现栈、括号匹配等),灵活运用特性解题。

无论是编程入门还是算法进阶,栈和队列都是必须扎实掌握的基础,其思想贯穿于后续的复杂数据结构(如树、图)和算法(如动态规划、贪心)中,打好基础才能稳步提升。


总结

以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

相关推荐
用户83562907805118 小时前
无需 Office:Python 批量转换 PPT 为图片
后端·python
日月云棠19 小时前
各版本JDK对比:JDK 25 特性详解
java
markfeng820 小时前
Python+Django+H5+MySQL项目搭建
python·django
用户83071968408220 小时前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide20 小时前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
GinoWi21 小时前
Chapter 2 - Python中的变量和简单的数据类型
python
IT探险家21 小时前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
JordanHaidee21 小时前
Python 中 `if x:` 到底在判断什么?
后端·python
花花无缺21 小时前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java
用户9083246027321 小时前
Spring Boot + MyBatis-Plus 多租户实战:从数据隔离到权限控制的完整方案
java·后端