你好,我是林森lsjs
我的Github 地址: sqyCoder (Qiyang) · GitHub
以博文记录成长,用心打磨代码与思维


目录
[2.1 定义语法](#2.1 定义语法)
[2.2 有哪些功能](#2.2 有哪些功能)
[第一套:单向链表实现队列 MyQueue(无傀儡头节点)](#第一套:单向链表实现队列 MyQueue(无傀儡头节点))
[LinkedNode 节点类(单向链表节点)](#LinkedNode 节点类(单向链表节点))
[MyQueue 成员变量](#MyQueue 成员变量)
[offer (int val) 入队方法【尾插,向队尾添加元素】](#offer (int val) 入队方法【尾插,向队尾添加元素】)
[poll () 出队方法【头删,删除并返回队首元素】](#poll () 出队方法【头删,删除并返回队首元素】)
[peek () 获取队首元素【只查看,不删除元素】](#peek () 获取队首元素【只查看,不删除元素】)
[main 测试方法执行流程模拟](#main 测试方法执行流程模拟)
[第二套:循环数组实现队列 MyArrayQueue](#第二套:循环数组实现队列 MyArrayQueue)
[构造方法 MyArrayQueue ()](#构造方法 MyArrayQueue ())
[offer (int val) 入队方法](#offer (int val) 入队方法)
[poll () 出队方法](#poll () 出队方法)
[peek () 查看队首元素](#peek () 查看队首元素)
[main 方法执行模拟](#main 方法执行模拟)
一、口诀
队列遵循先进先出 FIFO;
队尾用来入队新增元素,队首用来出队删除元素,新增、删除操作分开在两端执行。
offer完成尾插入队;p
oll头删出队,取出队首元素;
peek仅读取队首,不会改动队列内容。
链表实现队列维护 head 队首指针、tail 队尾指针;
数组实现采用循环数组,依靠取模运算形成环形,复用出队后闲置的数组空间。
二、用法
2.1 定义语法
ArrayDeque(推荐,无界数组队列,性能优于 LinkedList)
java
import java.util.Queue;
import java.util.ArrayDeque;
public class Test {
public static void main(String[] args) {
// 定义队列,存放Integer类型
Queue<Integer> queue = new ArrayDeque<>();
}
}
LinkedList(实现了 Queue 接口,链表队列)
java
import java.util.Queue;
import java.util.ArrayDeque;
public class Test {
public static void main(String[] args) {
// 定义队列,存放Integer类型
Queue<Integer> queue = new ArrayDeque<>();
}
}
2.2 有哪些功能
Java 原生Queue接口三个核心基础方法(本次不拓展 Deque)
-
offer(E e):入队,在队列尾部添加元素; -
poll():出队,删除并返回队首元素;队列为空时返回null; -
peek():获取队首元素,只查看、不删除;队列为空时返回null;核心区分:栈是同一端进出 ;队列一头进、另一头出。
三、手搓队列
第一套:单向链表实现队列 MyQueue(无傀儡头节点)
java
package queue;
class LinkedNode {
public int val;
public LinkedNode next;
public LinkedNode(int val) {
this.val = val;
this.next = null;
}
}
// 不引入傀儡节点
public class MyQueue {
private LinkedNode head = null;
// 为了尾插方便,引入尾结点
private LinkedNode tail = null;
// 入队列,实现尾插
public void offer(int val) {
LinkedNode newNode = new LinkedNode(val);
if (head == null) {
head = newNode;
tail = newNode;
return;
}
// 把新节点,添加到 tail 的末尾
tail.next = newNode;
tail = tail.next;
}
// 出队列,实现头删
public Integer poll() {
if (head == null) {
return null;
}
int ret = head.val;
head = head.next;
return ret;
}
// 取队首元素
public Integer peek() {
if (head == null) {
return null;
}
return head.val;
}
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
}
LinkedNode 节点类(单向链表节点)
java
class LinkedNode {
public int val;
public LinkedNode next;
public LinkedNode(int val) {
this.val = val;
this.next = null;
}
}
-
public int val:存储队列中保存的数据;使用public方便外部类直接访问,省去 get/set 方法。 -
public LinkedNode next:引用变量,保存下一个节点的内存地址。单向链表只能顺着 next 向后查找节点,无法反向查找。 -
构造方法
LinkedNode(int val):创建节点时必须传入存储的值。-
this.val = val:将传入数值赋值给当前节点; -
this.next = null:新创建的节点暂时没有连接任何其他节点,后继引用初始为空。
-
MyQueue 成员变量
private LinkedNode head = null;
private LinkedNode tail = null;
-
private:封装特性,外部代码不能直接修改头、尾指针,只能通过提供的方法操作队列。 -
head:指向队首节点。队列为空时 head = null;队列存在元素时,head 就是链表第一个节点。 -
tail:指向队尾节点。
重点:普通单向链表如果没有 tail 指针,每次尾部插入元素都要从 head 从头遍历到最后一个节点,时间复杂度 O (n);保存 tail 指针后,尾插直接操作 tail,时间复杂度 O (1),大幅提升效率。
- 空队列初始状态:
head = null,tail = null。
offer (int val) 入队方法【尾插,向队尾添加元素】
java
public void offer(int val) {
LinkedNode newNode = new LinkedNode(val);
if (head == null) {
head = newNode;
tail = newNode;
return;
}
tail.next = newNode;
tail = tail.next;
}
-
LinkedNode newNode = new LinkedNode(val);:根据传入的值,新建一个链表节点。 -
if (head == null)判断队列是否为空。- 队列为空:此时链表只有这唯一一个节点,队首和队尾是同一个节点,所以
head = newNode; tail = newNode;,执行 return 结束方法。
- 队列为空:此时链表只有这唯一一个节点,队首和队尾是同一个节点,所以
-
队列不为空的执行逻辑:
-
tail.next = newNode;:当前 tail 是最后一个节点,让尾节点的 next 指向新节点,完成节点连接; -
tail = tail.next;:移动 tail 指针,让 tail 指向新的末尾节点。
-
原生代码存在 BUG 当队列中最后一个元素被 poll 删除后,head会被赋值为 null,但是tail仍然保留旧节点引用。旧节点无法被垃圾回收,造成内存引用残留。修复方案:在 poll 方法中添加判断:
java
head = head.next;if(head == null){
tail = null;}
poll () 出队方法【头删,删除并返回队首元素】
java
public Integer poll() {
if (head == null) {
return null;
}
int ret = head.val;
head = head.next;
return ret;
}
-
if (head == null):队列为空,没有元素可以取出,返回 null; -
int ret = head.val;:提前保存队首节点的值。head 指针马上要向后移动,如果不提前保存,数据会丢失; -
head = head.next;:head 指针向后移动,指向第二个节点;原先的队首节点没有任何引用指向它,等待 JVM 垃圾回收; -
return ret;:返回取出的队首数值。
peek () 获取队首元素【只查看,不删除元素】
java
public Integer peek() {
if (head == null) {
return null;
}
return head.val;
}
判断队列非空,直接读取 head 节点存储的值。核心区别:peek 不会移动 head、tail 指针,链表结构完全不发生变化,仅仅读取数据。
main 测试方法执行流程模拟
java
MyQueue queue = new MyQueue(); // head=null,tail=null
queue.offer(1); // 队空,head=节点1,tail=节点1
queue.offer(2); // tail.next=节点2,tail指向节点2
queue.offer(3); // tail.next=节点3,tail指向节点3
queue.offer(4); // tail.next=节点4,tail指向节点4// 当前链表结构:1 → 2 → 3 → 4,head指向1,tail指向4System.out.println(queue.poll()); //取出1,head指向2,输出1System.out.println(queue.poll()); //取出2,head指向3,输出2System.out.println(queue.poll()); //取出3,head指向4,输出3System.out.println(queue.poll()); //取出4,head=null,输出4System.out.println(queue.poll()); //head为null,返回null
第二套:循环数组实现队列 MyArrayQueue
普通数组实现队列存在缺陷:元素出队后数组前面的空间永久浪费。
循环队列利用下标取模运算,让下标到达数组末尾后折返到下标 0,复用前面空闲空间。
本实现核心特点:使用size变量记录有效元素数量,区分空队列、满队列,不需要空出一个数组位置。
java
package queue;
public class MyArrayQueue {
private int[] array;
private int head;
private int tail;
private int size;
public MyArrayQueue() {
array = new int[1000];
head = 0;
tail = 0;
size = 0;
}
public void offer(int val) {
if (size == array.length) {
return;
}
array[tail] = val;
tail = (tail + 1) % array.length;
size++;
}
public Integer poll() {
if (size == 0) {
return null;
}
int ret = array[head];
head++;
if (head == array.length) {
head = 0;
}
size--;
return ret;
}
public Integer peek() {
if (size == 0) {
return null;
}
return array[head];
}
public static void main(String[] args) {
MyArrayQueue queue = new MyArrayQueue();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
}
成员变量逐行讲解
java
private int[] array;
private int head;
private int tail;
private int size;
-
int[] array:底层存储队列数据的数组,占用连续内存; -
head:队首元素所在数组下标,head 位置一定存在有效数据; -
tail:下一个待插入元素的下标位置!tail 指向的位置目前是空,没有数据(重点,极易混淆); -
size:队列当前有效元素个数。-
size == 0→ 队列为空 -
size == array.length→ 队列已满
-
构造方法 MyArrayQueue ()
java
public MyArrayQueue() {
array = new int[1000];
head = 0;
tail = 0;
size = 0;}
-
array = new int[1000]:创建长度固定为 1000 的数组,容量写死;拓展方向:可以实现动态扩容; -
head = 0; tail = 0:初始下标全部从 0 开始; -
size = 0:初始化队列没有任何元素。
offer (int val) 入队方法
java
public void offer(int val) {
if (size == array.length) {
return;
}
array[tail] = val;
tail = (tail + 1) % array.length;
size++;
}
-
if (size == array.length):有效元素数量等于数组最大容量,队列已满,无法继续添加元素,直接结束方法; -
array[tail] = val:把待存入的值放到 tail 下标位置; -
tail = (tail + 1) % array.length;取模实现循环:假设数组长度 1000,当tail = 999(数组最后一个下标),tail+1=1000,1000 % 1000 = 0,tail 自动回到下标 0;注释中 if 判断写法和取模效果完全一致,取模写法更加简洁通用; -
size++:成功新增元素,有效元素数量 + 1。
poll () 出队方法
java
public Integer poll() {
if (size == 0) {
return null;
}
int ret = array[head];
head++;
if (head == array.length) {
head = 0;
}
size--;
return ret;
}
-
if (size == 0):没有有效元素,队空,返回 null; -
int ret = array[head];:取出 head 下标对应的队首数据,提前保存; -
head++:队首下标向后移动一位; -
if (head == array.length) head = 0;:head 超出数组下标范围时,重置为 0,实现循环; -
size--:删除一个元素,有效元素数量减一; -
return ret:返回取出的数据。
peek () 查看队首元素
java
public Integer peek() {
if (size == 0) {
return null;
}
return array[head];
}
size 为 0 代表队空返回 null;否则直接读取 array head。head、tail、size 全部不会修改,仅查询数据。
main 方法执行模拟
java
MyArrayQueue queue = new MyArrayQueue(); // head=0 tail=0 size=0
queue.offer(1); // array[0]=1 tail=1 size=1
queue.offer(2); // array[1]=2 tail=2 size=2
queue.offer(3); // array[2]=3 tail=3 size=3
queue.offer(4); // array[3]=4 tail=4 size=4System.out.println(queue.poll()); //取array[0]=1 head=1 size=3 →输出1System.out.println(queue.poll()); //取array[1]=2 head=2 size=2 →输出2System.out.println(queue.poll()); //取array[2]=3 head=3 size=1 →输出3System.out.println(queue.poll()); //取array[3]=4 head=4 size=0 →输出4System.out.println(queue.poll()); //size=0 返回null
今天的数据结构讲解就到这了,我们下期再见!
++诸位共勉!++
