队列 FIFO 原理 & 手撕两种队列实现(链表 + 循环数组)—数据结构陆

你好,我是林森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)

  1. offer(E e):入队,在队列尾部添加元素;

  2. poll():出队,删除并返回队首元素;队列为空时返回null

  3. 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;
    }
}
  1. public int val:存储队列中保存的数据;使用public方便外部类直接访问,省去 get/set 方法。

  2. public LinkedNode next:引用变量,保存下一个节点的内存地址。单向链表只能顺着 next 向后查找节点,无法反向查找。

  3. 构造方法 LinkedNode(int val):创建节点时必须传入存储的值。

    1. this.val = val:将传入数值赋值给当前节点;

    2. this.next = null:新创建的节点暂时没有连接任何其他节点,后继引用初始为空。

MyQueue 成员变量

private LinkedNode head = null;

private LinkedNode tail = null;

  1. private:封装特性,外部代码不能直接修改头、尾指针,只能通过提供的方法操作队列。

  2. head:指向队首节点。队列为空时 head = null;队列存在元素时,head 就是链表第一个节点。

  3. tail:指向队尾节点

重点:普通单向链表如果没有 tail 指针,每次尾部插入元素都要从 head 从头遍历到最后一个节点,时间复杂度 O (n);保存 tail 指针后,尾插直接操作 tail,时间复杂度 O (1),大幅提升效率。

  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;
}
  1. LinkedNode newNode = new LinkedNode(val);:根据传入的值,新建一个链表节点。

  2. if (head == null) 判断队列是否为空。

    1. 队列为空:此时链表只有这唯一一个节点,队首和队尾是同一个节点,所以head = newNode; tail = newNode;,执行 return 结束方法。
  3. 队列不为空的执行逻辑:

    1. tail.next = newNode;:当前 tail 是最后一个节点,让尾节点的 next 指向新节点,完成节点连接;

    2. 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;
}
  1. if (head == null):队列为空,没有元素可以取出,返回 null;

  2. int ret = head.val;:提前保存队首节点的值。head 指针马上要向后移动,如果不提前保存,数据会丢失;

  3. head = head.next;:head 指针向后移动,指向第二个节点;原先的队首节点没有任何引用指向它,等待 JVM 垃圾回收;

  4. 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;
  1. int[] array:底层存储队列数据的数组,占用连续内存;

  2. head队首元素所在数组下标,head 位置一定存在有效数据;

  3. tail下一个待插入元素的下标位置!tail 指向的位置目前是空,没有数据(重点,极易混淆);

  4. size:队列当前有效元素个数。

    1. size == 0 → 队列为空

    2. size == array.length → 队列已满

构造方法 MyArrayQueue ()
复制代码
java 复制代码
public MyArrayQueue() {
    array = new int[1000];
    head = 0;
    tail = 0;
    size = 0;}
  1. array = new int[1000]:创建长度固定为 1000 的数组,容量写死;拓展方向:可以实现动态扩容;

  2. head = 0; tail = 0:初始下标全部从 0 开始;

  3. 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++;
}
  1. if (size == array.length):有效元素数量等于数组最大容量,队列已满,无法继续添加元素,直接结束方法;

  2. array[tail] = val:把待存入的值放到 tail 下标位置;

  3. tail = (tail + 1) % array.length;取模实现循环:假设数组长度 1000,当tail = 999(数组最后一个下标),tail+1=10001000 % 1000 = 0,tail 自动回到下标 0;注释中 if 判断写法和取模效果完全一致,取模写法更加简洁通用;

  4. 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;
}
  1. if (size == 0):没有有效元素,队空,返回 null;

  2. int ret = array[head];:取出 head 下标对应的队首数据,提前保存;

  3. head++:队首下标向后移动一位;

  4. if (head == array.length) head = 0;:head 超出数组下标范围时,重置为 0,实现循环;

  5. size--:删除一个元素,有效元素数量减一;

  6. 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

今天的数据结构讲解就到这了,我们下期再见!

++诸位共勉!++

相关推荐
何以解忧,唯有..1 小时前
Python time 模块详解:时间处理与日期操作指南
开发语言·python
caimouse1 小时前
ReactOS 窗口系统分析(20):光标与图标 — cursoricon.c
c语言·开发语言
H_oRIZoN_2 小时前
Linux入门DAY23(C语言树形结构)
linux·c语言·数据结构
lisin-lee-cooper2 小时前
简单聊聊 OpenFeign 框架源码
java·微服务
间歇性努力持续性发呆的野生快乐选手2 小时前
stl容器使用和算法简介
数据结构·c++
Buke..2 小时前
【APP 逆向】哔哩哔哩 sign 参数逆向(下):OLLVM 混淆还原 sign 算法
java·javascript·爬虫·python·算法
半生过往2 小时前
前端学 Java 课程笔记
java·前端·笔记
longxiaozhang62 小时前
C#运算符重载:让对象也能使用“加减乘除”
开发语言·c#
mmsx3 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp