队列简介 Queue

队列本质

队列跟栈一样,也是一种操作受限的线性表数据结构 ,只不过栈是是后进先出 (Last In First Out,LIFO),队列是先进先出(First In First Out,FIFO):

⨳ 把它想象成排成一队的人更容易理解,在队列中,处理总是从第一名开始往后进行,而新来的人只能排在队尾;

⨳ 也可以把它想象隧道也许,先进入隧道的车辆先出隧道,不能插队。

跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列,其主要API如下:

void enqueue(E):入队操作,即放一个数据到队列尾部

E dequeue():出队操作,即从队列头部取一个元素

E getFront():查看队列头部的元素

如果不考虑性能,只要阉割数组和链表的操作,就能实现队列,比如:

顺序队列 :数组头为队列头,数组尾为队列尾,向队列尾追加元素还好说,时间复杂度为 O(1),但在队列头拿数据(移除数组头部的元素)就麻烦了,这涉及到数据的搬移,时间复杂度为 O(n)

链式队列 :链表头节点为队列头,链表尾节点为队列尾,这时在队列头拿数据的时间复杂度为O(1),在队列尾追加元素的时间复杂度为 O(n) ,因为要遍历整个队列。

要想使链式队列的 enqueue 操作也是 O(1) ,可以再维护 tail 节点,很容易就实现了。

那顺序队列应该怎么改造呢,怎么让顺序队列的 enqueuedequeue 操作都是 O(1) 呢?

下面介绍循环队列。

循环队列

循环队列 是把顺序队列首尾相连,把线性表从逻辑上看成一个环,用head 指针和tail 指针标明队列的边界。

啥意思呢?

原本数组是有头有尾的,是一条直线,现在我们把首尾相连,扳成了一个环。假设 head 指针指向队列头,更精确点是队列中第一个元素,tail 指针指向队尾,更精确点是下一个元素入队的位置。

下图就是向循环队列队尾添加元素的示意图,即向tail 指针指向的位置添加元素:

从队头取元素,就是取head 指针指向的元素,如下图:

由上图可知,当 tail 指针追上 head 指针时,表示队列已满,当 head 指针追上 tail 指针时表示队列已空,现在仔细确认一下追上到底是什么意思:

队列为空条件 :初始情况下 headtail 都指向 arr[0],上图在队尾添加四个元素,又在队头取出四个元素,headtail 都指向 arr[4],也就是说当 head == tail 时表示队列为空。

队列已满条件 :初始情况下,在队尾添加四个元素时, head 指向 arr[0], tail 指向 arr[4],数组 arr[4] 还没有元素,如果此时还能在队尾追加元素,那会导致 tail 加一,指向 arr[0]headtail 都指向一个元素的情况不就是队列为空的条件嘛。

为了和队列为空条件作区分,只能浪费一个空间,让 tail 再向下走一步 等于 head 时为队列已满条件,又因为在循环队列中,索引 4 下一个位置是索引是 索引0,所以最终队列已满条件为 (tail + 1) % capacity,其中 capacity 为数组的容量。

初始化循环队列

还是以 int 数组为例构建循环队列:

java 复制代码
public class IntCircleQueue {

    private int[] data;
    private int head;  // 队头指针
    private int tail;  // 队尾指针

    public IntCircleQueue(int capacity){
        data = new int[capacity+1];// 因为要浪费一个空间,所以要加一
        head = 0;
        tail = 0;
    }
    public IntCircleQueue(){
        this(10); // 默认队列可存储十个元素
    }

}

入队操作

向循环队列队尾添加元素,就是向tail 指针指向的位置添加元素:

java 复制代码
public void enqueue(int e){

    if((tail + 1) % data.length == head)
        throw new IllegalArgumentException("enqueue failed. Queue is full.");

    data[tail] = e;
    tail = (tail + 1) % data.length;
}

这里当 tail 追上 head 时,报了一个队列已满的错,完全可以引入动态数组的思想,构造一个更大的数组:

java 复制代码
public void enqueue(int e){

    if((tail + 1) % data.length == head){
        //throw new IllegalArgumentException("enqueue failed. Queue is full.");
        int[] new_data = new int[(data.length-1)*2+1]; // 两倍扩容
        for(int i = 0 ; i < data.length ; i ++)
            new_data[i] = data[i];
        data = new_data;
    }


    data[tail] = e;
    tail = (tail + 1) % data.length;
}

出队操作

从队头取元素,就是取head 指针指向的元素:

java 复制代码
public int dequeue(){

    if(head == tail)
        throw new IllegalArgumentException("Cannot dequeue from an empty queue.");

    int ret = data[head];
    head = (head + 1) % data.length;
    return ret;
}

不想浪费一个空间

如果队满的时候不想浪费那一个 tail 指向的空间怎么办?

引入一个 size 属性即可,size 表示队列中的元素个数,入队的时候 size++,出队的时候 size--,当 size == 0 时,队列已空,当 size == data.length 时,队列已满,简单粗暴。

双端队列

前文讲栈的时候,提到Java官方推荐使用 Deque 代替 StackDeque 就是双端队列,既可以在队尾添加元素又可以在队尾取出元素,既可以在队头取出元素,又可以在队头插入元素。

也就是说双端队列的两端,都可以进行数据的存取。

为了方便实现,这里使用带有 size 属性的队列进行改造:

java 复制代码
public class IndDeque {

    private int[] data;
    private int head;  // 队头指针
    private int tail;  // 队尾指针
    
    private int size; // 队列中元素的个数
    
    public IndDeque(int capacity){
        data = new int[capacity];
        head = 0;
        tail = 0;
    }
    public IndDeque(){
        this(10); // 默认队列可存储十个元素
    }
}

在队尾添加元素,就是在 tail 指针指向位置添加元素,那在队尾取元素,就是取 tail 指针上一个位置的数据:

java 复制代码
public int removeLast(){

    if(size == 0) // 如果队列为空
        throw new IllegalArgumentException("Cannot removeLast from an empty queue.");

    // 计算删除掉队尾元素以后,新的 tail 位置
    tail = tail == 0 ? data.length - 1 : tail - 1;
    size --;
    return data[tail];
}

在队头取元素,就是取 head 指针指向的元素,那在队头插入元素,就要在 head 指针上一个位置插入数据:

java 复制代码
public void addFront(int e){

    if(size == data.length)
        throw new IllegalArgumentException("addFront failed. Queue is full.");
    // 我们首先需要确定添加新元素的索引位置
    // 这个位置是 head - 1 的地方
    // 但是要注意,如果 head == 0,新的位置是 data.length - 1 的位置
    head = head == 0 ? data.length - 1 : head - 1;
    data[head] = e;
    size ++;
}
相关推荐
哎呦没2 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
Kalika0-019 分钟前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
编程、小哥哥29 分钟前
netty之Netty与SpringBoot整合
java·spring boot·spring
sp_fyf_20241 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-02
人工智能·神经网络·算法·计算机视觉·语言模型·自然语言处理·数据挖掘
IT学长编程2 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码2 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
我是哈哈hh2 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
郭二哈2 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃2 小时前
SpringBoot的数据访问
java·spring boot·后端