数据结构与算法—搞懂队列

news2025/1/16 1:43:26

csdn专栏:数据结构与算法

前言

栈和队列是一对紧密相关的数据结构。之前已经介绍过栈(它遵循后进先出的原则),栈的机制相对简单,就像你进入一个狭窄的山洞,山洞只有一个出入口,因此你只能按照后进先出的顺序离开。这意味着最后进入山洞的人会最先离开,而先进入的人需要等待。这就是栈的工作原理。

而队列就好比是一个隧道,人们按照先来后到的顺序排队前进,而最早排队的人会首先通过隧道。队列的运作方式被称为先入先出,即先进入队列的元素将会先离开。

栈是一种喜新厌旧的数据结构,新数据到来时,会处理新数据,而老数据将被推迟处理。因此栈的一些使用场景可能会遇到一些饥饿的情况。

队列则是一种公平的数据结构,它按照先后顺序处理数据,非常注重顺序性。因此,队列在程序设计、中间件等领域都得到了广泛应用,例如消息队列、FIFO磁盘调度、二叉树的层序遍历、BFS搜索等等。

队列的核心理念可以用简单的一句话来表达:先进先出

队列是一种特殊的线性表,其特殊之处在于只允许在表的前端(队头)进行删除操作,而在表的后端(队尾)进行插入操作。和栈一样,队列也是一种操作受限制的线性表。在队列中,进行插入操作的一端称为队尾,而进行删除操作的一端称为队头。

在学习队列前,最好先了解顺序表的基本操作和栈的数据结构。这样可以更好地理解队列的概念和工作原理,并提高学习效果。

队列介绍

我们设计队列时候可以选择一个标准:

队头front: 删除数据的一端。

队尾rear: 插入数据的一端。

对于数组,从数组后面插入更容易,数组前面插入较困难,所以一般用数组实现的队列队头在数组前面,队尾在数组后面;而对于链表,插入删除在两头分别进行那么头部(前面)删除尾部插入最方便的选择。

image-20231106212743098

实现一个队列的接口:

public interface Queue<T> {
    // 向队列尾部插入元素,如果成功插入则返回 true,否则返回 false
    boolean enQueue(T item);

    // 从队列头部删除一个元素,如果成功删除则返回 true,否则返回 false
    boolean deQueue();

    // 返回队列头部的元素,如果队列为空,返回 null
    T peek();

    // 检查队列是否为空
    boolean isEmpty();

    // 返回队列的大小
    int size();
}

循环队列(数组实现)

普通队列并不能满足使用需求,因为用数组实现的普通队列不论是入队还是出队都往一个方向操作,这样会导致很快到达数组的末尾。

为了解决普通队列资源利用率很低的问题,可以使用数组实现一个循环队列提高数组的利用率,具体的实现方式是对每个位置操作时候对实际地址取余这样尾和首就实现了逻辑上的连续。

实现循环队列,大致知道其原理,就要针对一些细节进行推敲以及定义,避免模糊。

front:表示队头,头位置元素出,定义array[front]为队列第一个元素位置。

rear:表示队尾,尾位置用来插入,定义array[tail]为队列最后一个元素位置。

image-20231106231314500

初始化:刚开始由于是空的,front为0,rear为-1,不过怎么样定义front和rear并不位于,需要处理好入队、出队、判空、判满的逻辑即可。

入队enQueue:队不满,先队尾逻辑上后移一位rear=(rear + 1) % arrLength;然后赋值,size++

出队deQueue:队不空,队头逻辑上后移一位,front=(front + 1)% arrLength;然后size–

这里出队入队指标相加如果遇到最后需要转到头位置,这里直接+1求余找到位置(相比判断是否在最后更加简洁),其中arrLength是数组实际大小。

取队头peek:队不空,返回array[front]

是否为空:size是否为0

具体实现:

public class ArrayQueue<T> implements Queue<T> {
    private T[] array;
    private int front; // 指向队列头部
    private int rear; // 指向队列尾部
    private int size;

    public ArrayQueue(int capacity) {
        array = (T[]) new Object[capacity];
        front = 0;
        rear = -1;
        size = 0;
    }

    @Override
    public boolean enQueue(T item) {
        if (size == array.length) {
            // 队列已满
            return false;
        }
        rear = (rear + 1) % array.length; // 循环队列,计算新的尾部位置
        array[rear] = item;
        size++;
        return true;
    }

    @Override
    public boolean deQueue() {
        if (isEmpty()) {
            // 队列为空
            return false;
        }
        front = (front + 1) % array.length; // 循环队列,移动头部指针
        size--;
        return true;
    }

    @Override
    public T peek() {
        if (isEmpty()) {
            return null;
        }
        return array[front];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return size;
    }
}

循环队列(链表实现)

我们知道队列是先进先出的,对于链表,我们能采用单链表尽量采用单链表,能方便尽量方便,同时还要兼顾效率。使用链表大概有两个实现方案:

方案一 如果队列头设在链表尾,队列尾设在链表头。那么队尾进队插入在链表头部插入没问题,容易实现,但是如果队头删除在链表尾部进行,如果不设置尾指针要遍历到队尾,但是设置尾指针删除需要将它前驱节点需要双向链表,都挺麻烦的。

方案二如果队列头设在链表头,队列尾设在链表尾,那么队尾进队插入在链表尾部插入没问题(用尾指针可以直接指向next),容易实现,如果队头删除在链表头部进行也很容易,就是我们前面常说的头节点删除节点

所以我们最终采取的是方案二,其中链表的头部表示队头,链表的尾部表示队尾。入队操作在队尾插入元素,出队操作在队头删除元素,通过维护 headtail 指针来管理队列。希望这个示例满足你的需求。

主要操作为:

初始化:head和tail均为null,size为0;

入队

  • 创建新节点

  • 如果队列为空(tail为null),head和tail都指向新节点

  • 如果队列不为空,tail的next指向新节点,tail指向新节点

  • size++

image-20231107002738965

出队

  • 如果不为空,移除链表头节点
  • head指向head后面(head.next)表示删除
  • size–
  • 如果head为null,tail也设为null(此时表示队列已经空了)

image-20231107001204876

实现代码:

public class LinkedQueue<T> implements Queue<T> {
    private Node<T> head; // 队列头部
    private Node<T> tail; // 队列尾部
    private int size;

    public LinkedQueue() {
        head = null;
        tail = null;
        size = 0;
    }

    @Override
    public boolean enQueue(T item) {
        Node<T> newNode = new Node<>(item);
        if (isEmpty()) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode; // 将新元素添加到队列尾部
            tail = newNode; // 更新队列尾部指针 也可携程tail=tail.next
        }
        size++;
        return true;
    }

    @Override
    public boolean deQueue() {
        if (isEmpty()) {
            return false;
        }
        head = head.next; // 移除队列头部的元素
        size--;
        if (head == null) {
            // 如果队列为空,将尾部也设置为null
            tail = null;
        }
        return true;
    }

    @Override
    public T peek() {
        if (isEmpty()) {
            return null;
        }
        return head.data; // 返回队列头部的元素数据
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int size() {
        return size;
    }

    private static class Node<T> {
        T data;
        Node<T> next;

        Node(T data) {
            this.data = data;
            this.next = null;
        }
    }
}

双向队列(加餐)

设计实现双端队列,其实你经常使用的ArrayDeque就是一个经典的双向队列,其基于数组实现,效率非常高。我们这里实现的双向队列模板基于力扣641 设计循环双端队列 。
你的实现需要支持以下操作:

  • MyCircularDeque(k):构造函数,双端队列的大小为k。
  • insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
  • insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
  • deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
  • deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
  • getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
  • getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
  • isEmpty():检查双端队列是否为空。
  • isFull():检查双端队列是否满了。

其实有了上面的基础,实现一个双端队列非常容易,有很多操作和单端的循环队列是一致的,只有多了一个队头插入队尾删除的操作,两个操作分别简单的分析一下:

队头插入:队友front下标位置本身是有值的,所以要将front退后一位然后再赋值,不过要考虑是否为满或者数组越界情况。

队尾删除:只需要rear位置减1,同时也要考虑是否为空和越界情况。

具体实现代码:

public class MyCircularDeque {
    private int data[];// 数组容器
    private int front;// 头
    private int rear;// 尾
    private int maxsize;// 最大长度

    /*初始化 最大大小为k */
    public MyCircularDeque(int k) {
        data = new int[k + 1];
        front = 0;
        rear = 0;
        maxsize = k + 1;
    }

    /**
     * 头部插入
     */
    public boolean insertFront(int value) {
        if (isFull()) {
            return false;
        } else {
            front = (front + maxsize - 1) % maxsize;
            data[front] = value;
        }
        return true;
    }

    /**
     * 尾部插入
     */
    public boolean insertLast(int value) {
        if (isFull()) {
            return false;
        } else {
            data[rear] = value;
            rear = (rear + 1) % maxsize;
        }
        return true;
    }

    /**
     * 正常头部删除
     */
    public boolean deleteFront() {
        if (isEmpty()) {
            return false;
        } else {
            front = (front + 1) % maxsize;
        }
        return true;
    }

    /**
     * 尾部删除
     */
    public boolean deleteLast() {
        if (isEmpty()) {
            return false;
        } else {
            rear = (rear + maxsize - 1) % maxsize;
        }
        return true;
    }

    /**
     * Get the front item
     */
    public int getFront() {
        if (isEmpty()) {
            return -1;
        }
        return data[front];
    }

    /**
     * Get the last item from the deque.
     */
    public int getRear() {
        if (isEmpty()) {
            return -1;
        }
        return data[(rear - 1 + maxsize) % maxsize];
    }

    /**
     * Checks whether the circular deque is empty or not.
     */
    public boolean isEmpty() {
        return front == rear;
    }

    /**
     * Checks whether the circular deque is full or not.
     */
    public boolean isFull() {
        return (rear + 1) % maxsize == front;
    }
}

总结

队列相比栈数据结构复杂一些,要记住先进先出的规则,然后用数组或者链表实现即可。

数组实现循环队列,主要是要注意循环取余的妙用,链表实现队列需要考虑为空状态。

而双向队列则是既能当队列又能当栈的一种高效数据结构,掌握还是很有必要的。

系列仓库地址:https://github.com/javasmall/bigsai-algorithm 欢迎star
csdn专栏:数据结构与算法

写一篇原创不易,还请点赞、收藏、关注三连支持一下!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1197720.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【原创分享】Mentor PADS将PCB封装直接添加到PCB的教程

一般&#xff0c;批量添加封装到PCB板上有以下方法&#xff1a; 第一步&#xff1a;点击菜单栏“ECO模式--添加元器件”如图&#xff0c;点击以后弹出如图界面。 1&#xff09;元件类型 PCB封装必须得添加完元件类型&#xff0c;才能通过ECO模式添加到PCB界面里面&#xff0c…

[Linux打怪升级之路]-信号的保存和递达

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、信号的保…

红黑树-RBTree

目录 1. 红黑树的概念2. 红黑树的性质3. 结点的定义4. 结点的插入5. 整体代码 1. 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式…

基于51单片机RFID射频门禁刷卡系统设计

**单片机设计介绍&#xff0c; 基于51单片机RFID射频门禁刷卡系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序程序 六、 文章目录 一 概要 基于51单片机RFID射频门禁刷卡系统&#xff0c;是一种将单片机技术和射频标识技术应用于门禁控制系统的…

34 Feign最佳实践

2.4.2.抽取方式 将Feign的Client抽取为独立模块&#xff0c;并且把接口有关的POJO、默认的Feign配置都放到这个模块中&#xff0c;提供给所有消费者使用。 例如&#xff0c;将UserClient、User、Feign的默认配置都抽取到一个feign-api包中&#xff0c;所有微服务引用该依赖包…

一篇文章真正讲懂模型评估指标(准确率,召回率,精确率,roc曲线,AUC值)

背景&#xff1a; 最近在做一些数据分析的比赛的时候遇到了一些头疼的问题&#xff0c;就是我们如何评估一个模型的好坏呢&#xff1f; 准确率&#xff0c;召回率&#xff0c;精确率&#xff0c;roc曲线&#xff0c;roc值等等&#xff0c;但是模型评估的时候用哪个指标呢&…

[工业自动化-12]:西门子S7-15xxx编程 - PLC从站 - ET200 SP系列详解

目录 一、概述 1.1 概述 二、系统组成 2.1 概述 2.2 与主站的通信接口模块 2.3 总线适配器 2.4 基座单元 2.5 电子模块 2.6 服务器模块 一、概述 1.1 概述 PLC ET200 SP 是西门子&#xff08;Siemens&#xff09;公司生产的一款模块化可编程逻辑控制器&#xff08;PL…

初探SVG

SVG&#xff0c;可缩放矢量图形&#xff08;Scalable Vector Graphics&#xff09;。使用XML格式定义图像。SVG有以下优点&#xff1a;1&#xff09;可被非常多的工具读取和修改&#xff1b;2&#xff09;比JPEG和GIF尺寸更小&#xff0c;可压缩性更强&#xff1b;3&#xff09…

科力雷达Lidar使用指南

科力2D Lidar使用指南 作者&#xff1a; Herman Ye Galbot Auromix 版本&#xff1a; V1.0 测试环境&#xff1a; Ubuntu20.04(x86) PC 以及 Ubuntu20.04(Arm) Nvidia Orin 更新日期&#xff1a; 2023/11/11 注1&#xff1a; 本文内容中的硬件由 Galbot 提供支持。 注2&#x…

物联网AI MicroPython学习之语法uzlib解压缩

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; uzlib 介绍 uzlib 模块解压缩用DEFLATE算法压缩的二进制数据 &#xff08;通常在zlib库和gzip存档器中使用&#xff09;&#xff0c;压缩功能尚未实现。 注意&#xff1a;解压缩前&#xff0c;应检查模块内可…

C语言——个位数为 6 且能被 3 整除但不能被 5 整除的三位自然数共有多少个,分别是哪些?

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int i,j0;for(i100;i<1000;i) {if(i%106&&i%30&&i%5!0){printf("%6d",i); j;}}printf("\n一共%d个\n",j);return 0; } %6d起到美化输出格式的作用&#xff…

(一)QML加载离线地图+标记坐标点

1、实现效果 加载离线地图瓦片、鼠标拖拽、滚轮缩放在地图上固定坐标位置标注地名 &#xff08;一&#xff09;QML加载离线地图标记坐标点&#xff1a;MiniMap-mini 2、实现方法 2.1、使用工具下载离线地图 不废话&#xff0c;直接搬别人的砖&#xff0c;曰&#xff1a;他山…

jbase实现申明式事务

对有反射的语言&#xff0c;申明式事务肯定不可少。没必要没个人都try&#xff0c;catch写事务&#xff0c;写的不好的话还经常容易锁表&#xff0c;为此给框架引入申明式事务。申明式既字面意思&#xff0c;在需要事务的方法前面加一个申明&#xff0c;那么框架保证事务。 首…

比亚迪推动启动电池无铅化 车主有福了

时间过得很快&#xff0c;又到了立冬&#xff0c;意味着冬季已经开始。此时的北方已经下起了大雪&#xff0c;即便是艳阳高照的粤北山区&#xff0c;早晚也出现了较大的温差。笔者不禁想起此前做二手车时期的尴尬场面——三年以上的老车&#xff0c;尤其是没有更换过启动电池的…

38 路由的过滤器配置

3.3.断言工厂 我们在配置文件中写的断言规则只是字符串&#xff0c;这些字符串会被Predicate Factory读取并处理&#xff0c;转变为路由判断的条件 例如Path/user/**是按照路径匹配&#xff0c;这个规则是由 org.springframework.cloud.gateway.handler.predicate.PathRoute…

Python实现局部二进制算法(LBP)

1.介绍 局部二进制算法是一种用于获取图像纹理的算法。这算法可以应用于人脸识别、纹理分类、工业检测、遥感图像分析、动态纹理识别等领域。 2.示例 """ 局部二进制算法&#xff0c;计算图像纹理特征 """ import cv2 import numpy as np imp…

Java自学第9课:JSP基础及内置对象

目录&#xff1a; 目录 1 JSP基础知识架构 1 指令标识 1 Page命令 2 Including指令 3 taglib指令 2 脚本标识 1 JSP表达式 2 声明标识 3 代码片段 3 JSP注释 1 HTML注释 2 带有JSP表达式的注释 3 隐藏注释 4 动态注释 4 动作标识 1 包含文件标识 2 请求转发标…

写在 Chappyz 即将上所之前:基于 AI 技术对 Web3 营销的重新定义

前不久&#xff0c;一个叫做 Chappyz 的项目&#xff0c;其生态代币 $CHAPZ 在 Seedify、Poolz、Decubate、ChainGPT、Dao Space 等几大 IDO 平台实现了上线后几秒售罄&#xff0c;并且 Bitget、Gate.io、PancakeSwap 等几大平台也纷纷表示支持&#xff0c;并都将在 11 月 13 日…

浅析移动端车牌识别技术的工作原理及其过程

随着社会经济的发展与汽车的日益普及带来巨大的城市交通压力,在此背景下,智能交通系统成为解决这一问题的关键。而在提出发展无线智能交通系统后,作为智能交通的核心,车牌识别系统需要开始面对车牌识别移动化的现实需求。基于实现车牌识别移动化这一目标,一种基于Android移动终…

报时机器人的rasa shell执行流程分析

本文以报时机器人为载体&#xff0c;介绍了报时机器人的对话能力范围、配置文件功能和训练和运行命令&#xff0c;重点介绍了rasa shell命令启动后的程序执行过程。 一.报时机器人项目结构 1.对话能力范围 (1)能够识别欢迎语意图(greet)和拜拜意图(goodbye) (2)能够识别时间意…