排好队,一个一个来:宫本武藏教你学队列(附各种队列源码)

文章目录

    • 前言:
    • 理解“队列”的正确姿势
    • 一个关于队列的小思考——请求处理
    • 队列的两大“护法”————顺序队列和链式队列
      • 数组实现的队列
      • 链表实现的队列
    • 循环队列
    • 关于开篇,你明白了吗?
    • 最后说一句

前言:

哈喽!欢迎来到黑洞晓威的博客!

上一次我们在这里聊了一下队列,现在,让我们再次翻开这个话题,继续探讨一下这个有趣的数据结构吧!
虽然队列看起来比较普通,但是它在实际应用中却 有着不可替代的作用。所以,无论是计算机系统中的任务调度,还是网络数据包的传输,队列都扮演着重要的角色。
接下来,我们将深入了解队列的应用、实现以及相关算法问题。让我们一起来暴打队列吧!

在这里插入图片描述

理解“队列”的正确姿势

王者荣耀中宫本武藏有这么一句台词——“想挑战的人排好队,一个一个来”。
这句台词可以很好地联系到数据结构中的队列。在数据结构中,队列就像是一群人在排队等待挑战宫本武藏,每个人都必须按照先来后到的原则,依次接受服务。当新的人加入队伍时,必须排在队尾,而队伍中的人只能按照先后顺序依次出队。

在这里插入图片描述

我们知道,栈只支持两个基本操作: 入栈push()和出栈pop()。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个: 入队enqueue(),放一个数据到队列尾部; 出队dequeue(),从队列头部取一个元素。

在这里插入图片描述

所以,队列跟栈一样,也是一种 操作受限的线性表数据结构

队列的概念很好理解,基本操作也很容易掌握。作为一种非常基础的数据结构,队列的应用也非常广泛,比如排队、缓存、广度优先搜索等等。你准备好了吗?让我们一起来探索队列的奥秘吧!

一个关于队列的小思考——请求处理

我们知道,CPU资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致CPU频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。

当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?

实际上,这些问题并不复杂,其底层的数据结构就是我们今天要学的内容,队列(queue)。

队列的两大“护法”————顺序队列和链式队列

嘿,大佬们!我们现在知道了,队列跟栈一样,也是一种抽象的数据结构。它的特性很简单,就是先进先出,支持在队尾插入元素,在队头删除元素。但是,究竟

该如何实现一个队列呢?

让我们来看看吧!就像栈一样,队列也可以用数组来实现,这种实现方式叫做顺序队列。还可以用链表来实现,这种实现方式叫做链式队列。顺序队列和链式队列都有各自的优缺点,需要根据实际情况进行选择。所以,无论是顺序队列还是链式队列,都是我们实现队列的可选方案。现在,让我们先来看下基于数组的实现方法吧!

数组实现的队列

// 用数组实现的队列
public class ArrayQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

  // 申请一个大小为capacity的数组
  public ArrayQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入队
  public boolean enqueue(String item) {
    // 如果tail == n 表示队列已经满了
    if (tail == n) return false;
    items[tail] = item;
    ++tail;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
    String ret = items[head];
    ++head;
    return ret;
  }
}

比起栈的数组实现,队列的数组实现稍微有点儿复杂,但是没关系。我稍微解释一下实现思路,你很容易就能明白了。

对于栈来说,我们只需要一个 栈顶指针 就可以了。但是队列需要两个指针:一个是head指针,指向队头;一个是tail指针,指向队尾。

你可以结合下面这张图来理解。当a、b、c、d依次入队之后,队列中的head指针指向下标为0的位置,tail指针指向下标为4的位置。

在这里插入图片描述

当我们调用两次出队操作之后,队列中head指针指向下标为2的位置,tail指针仍然指向下标为4的位置。

在这里插入图片描述

你肯定已经发现了,随着不停地进行入队、出队操作,head和tail都会持续往后移动。当tail移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。这个问题该如何解决呢?

你是否还记得,在数组那一节,我们也遇到过类似的问题,就是数组的删除操作会导致数组中的数据不连续。你还记得我们当时是怎么解决的吗?对,用 数据搬移!但是,每次进行出队操作都相当于删除数组下标为0的数据,要搬移整个队列中的数据,这样出队操作的时间复杂度就会从原来的O(1)变为O(n)。能不能优化一下呢?

实际上,我们在出队时可以不用搬移数据。如果没有空闲空间了,我们只需要在入队时,再集中触发一次数据的搬移操作。借助这个思想,出队函数dequeue()保持不变,我们稍加改造一下入队函数enqueue()的实现,就可以轻松解决刚才的问题了。下面是具体的代码:

   // 入队操作,将item放入队尾
  public boolean enqueue(String item) {
    // tail == n表示队列末尾没有空间了
    if (tail == n) {
      // tail ==n && head==0,表示整个队列都占满了
      if (head == 0) return false;
      // 数据搬移
      for (int i = head; i < tail; ++i) {
        items[i-head] = items[i];
      }
      // 搬移完之后重新更新head和tail
      tail -= head;
      head = 0;
    }

    items[tail] = item;
    ++tail;
    return true;
  }

从代码中我们看到,当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将head到tail之间的数据,整体搬移到数组中0到tail-head的位置。

在这里插入图片描述

这种实现思路中,出队操作的时间复杂度仍然是O(1),但入队操作的时间复杂度还是O(1)吗?你可以用我们第3节、第4节讲的算法复杂度分析方法,自己试着分析一下。

接下来,我们再来看下 基于链表的队列实现方法

链表实现的队列

基于链表的实现,我们同样需要两个指针:head指针和tail指针。它们分别指向链表的第一个结点和最后一个结点。如图所示,入队时,tail->next= new_node, tail = tail->next;出队时,head = head->next。我将具体的代码放到GitHub上,你可以自己试着实现一下,然后再去GitHub上跟我实现的代码对比下,看写得对不对。

首先,我们需要定义一个节点类,它包含一个值属性和一个指向下一个节点的指针属性:

public class Node<T> { // 这是一个泛型类,T表示节点的值可以是任何类型
    T value; // 存储节点的值
    Node<T> next; // 指向下一个节点的指针
    
    public Node(T value) { // 构造函数,用来创建新的节点
        this.value = value; // 设置节点的值
        this.next = null; // 初始时,指针为空
    }
}

接下来,我们定义一个队列类,它包含一个指向队列头部的指针属性和一个指向队列尾部的指针属性:

public class Queue<T> { // 这是一个泛型类,T表示队列的元素可以是任何类型
    Node<T> head; // 指向队列头部的指针
    Node<T> tail; // 指向队列尾部的指针
    
    public Queue() { // 构造函数,用来创建新的队列
        this.head = null; // 初始时,头部指针为空
        this.tail = null; // 初始时,尾部指针为空
    }
}

在队列类中,我们可以实现以下方法:

  1. enqueue(value):将一个元素添加到队列的尾部。
public void enqueue(T value) { // 将元素value添加到队列的尾部
    Node<T> newNode = new Node<>(value); // 创建一个新的节点
    if (tail == null) { // 如果队列为空
        head = newNode; // 将头部指针指向新节点
        tail = newNode; // 将尾部指针指向新节点
    } else { // 如果队列不为空
        tail.next = newNode; // 将当前尾部节点的指针指向新节点
        tail = newNode; // 将尾部指针指向新节点
    }
}

  1. dequeue():从队列的头部移除并返回一个元素。
public T dequeue() { // 从队列的头部移除并返回一个元素
    if (head == null) { // 如果队列为空
        return null; // 返回null
    }
    T value = head.value; // 获取队列头部节点的值
    head = head.next; // 将头部指针指向下一个节点
    if (head == null) { // 如果队列为空
        tail = null; // 将尾部指针也置为空
    }
    return value; // 返回队列头部节点的值
}

  1. peek():返回队列头部的元素,但不移除它。
public T peek() { // 返回队列头部的元素,但不移除它
    if (head == null) { // 如果队列为空
    return null; // 返回null
	}
 return head.value; // 返回队列头部节点的值
}
  1. isEmpty():检查队列是否为空。
public boolean isEmpty() { // 检查队列是否为空
    return head == null; // 如果头部指针为空,队列为空
}
  1. clear():清空队列。
public void clear() { // 清空队列
    head = null; // 将头部指针置为空
    tail = null; // 将尾部指针置为空
}

在这里插入图片描述

在这里偷偷告诉大家,基于链表的队列我懒得写了于是就交给了ChatGPT完成,说实话效果竟然出奇的好,大家在学习这些基础内容的时候也不妨善用ChatGPT,说必定你就能解锁一个优质的老师哈哈哈。

在这里插入图片描述

循环队列

我们刚才用数组来实现队列的时候,在tail==n时,会有数据搬移操作,这样入队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?我们来看看循环队列的解决思路。

循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。我画了一张图,你可以直观地感受一下。

在这里插入图片描述

我们可以发现,图中这个队列的大小为8,当前head=4,tail=7。当有一个新的元素a入队时,我们放入下标为7的位置。但这个时候,我们并不把tail更新为8,而是将其在环中后移一位,到下标为0的位置。当再有一个元素b入队时,我们将b放入下标为0的位置,然后tail加1更新为1。所以,在a,b依次入队之后,循环队列中的元素就变成了下面的样子:

在这里插入图片描述

通过这样的方法,我们成功避免了数据搬移操作。看起来不难理解,但是循环队列的代码实现难度要比前面讲的非循环队列难多了。要想写出没有bug的循环队列的实现代码,我个人觉得,最关键的是, 确定好队空和队满的判定条件

在用数组实现的非循环队列中,队满的判断条件是tail == n,队空的判断条件是head == tail。那针对循环队列,如何判断队空和队满呢?

队列为空的判断条件仍然是head == tail。但队列满的判断条件就稍微有点复杂了。我画了一张队列满的图,你可以看一下,试着总结一下规律。

在这里插入图片描述

就像我图中画的队满的情况,tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。多画几张队满的图,你就会发现,当队满时, (tail+1)%n=head

你有没有发现,当队列满时,图中的tail指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。

Talk is cheap,如果还是没怎么理解,那就show you code吧。

public class CircularQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

  // 申请一个大小为capacity的数组
  public CircularQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入队
  public boolean enqueue(String item) {
    // 队列满了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    String ret = items[head];
    head = (head + 1) % n;
    return ret;
  }
}

关于开篇,你明白了吗?

队列的知识就讲完了,我们现在回过来看下开篇的问题。线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何实现的呢?
在这里插入图片描述

我们一般有两种处理策略。

第一种是非阻塞的处理方式,直接拒绝任务请求;

另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。那如何存储排队的请求呢?

我们希望公平地处理每个排队的请求,先进者先服务,所以队列这种数据结构很适合来存储排队请求。我们前面说过,队列有基于链表和基于数组这两种实现方式。这两种实现方式对于排队请求又有什么区别呢?

基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。

而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。
不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。

最后说一句

感谢大家的阅读,文章通过网络学习资源以及自己的学习过程整理出来,希望能帮助到大家。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以提出来,我会对其加以修改。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

货物摆放 (蓝桥杯) JAVA

题目描述 小蓝有一个超大的仓库&#xff0c;可以摆放很多货物。 现在&#xff0c;小蓝有n 箱货物要摆放在仓库&#xff0c;每箱货物都是规则的正方体。 小蓝规定了长、宽、高三个互相垂直的方向&#xff0c;每箱货物的边都必须严格平行于长、宽、高。 小蓝希望所有的货物最终摆…

可换皮肤的Qt登录界面

⭐️我叫忆_恒心,一名喜欢书写博客的在读研究生👨‍🎓。 如果觉得本文能帮到您,麻烦点个赞👍呗! 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧,喜欢的小伙伴给个三连支持一下呗。👍⭐️❤️ 可换皮肤的Qt登录界面 QSS的学习笔记 快…

真是一篇神奇的文章 动图DIY - 动图剪辑 - 录屏 - 画画动图 (无需VIP的软件推荐)

很多人想自己去搞一些 动图 &#xff0c;去了好多网址或者app&#xff0c;不是收费 &#x1f4b4; 就是 VIP &#x1f511; &#xff0c;博主也苦恼了好久&#xff0c;今天遇到一款超级、无敌、好用的软件 ScreeTogif &#xff0c;下载连接 &#x1f617;&#x1f619;&#x1…

高通开发系列 - Sensors Bring Up

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 返回高通开发系列 - 总目录 目录 问题背景高通android sensor信息Sensors Execution Environment (SEE)qxdm抓sensor log的方法android 调试…

Hadoop之Mapreduce序列化

目录 什么是序列化&#xff1a; 什么是反序列化&#xff1a; 为什么要序列化&#xff1a; Java的序列化&#xff1a; Hadoop序列化: 自定义序列化接口&#xff1a; 实现序列化的步骤&#xff1a; 先看源码进行简单分析&#xff1a; 序列化案例实操&#xff1a; 案例需…

蓝桥杯真题——自动售水机

2012年第四届全国电子专业人才设计与技能大赛“自动售水机”设计任务书1. 系统框图接下来我们将任务分块&#xff1a; 1. 按键控制单元 设定按键 S7 为出水控制按键&#xff0c;当 S7 按下后&#xff0c;售水机持续出水&#xff08;继电器接通&#xff0c;指示 灯 L10 点亮&…

Java中的JSON序列化和反序列化

文章目录Java 和 JSON 序列化JSON 简介JSON 是什么JSON 标准JSON 优缺点JSON 工具Java JSON 库JSON 编码指南Fastjson 应用添加 maven 依赖Fastjson API定义 Bean序列化反序列化Fastjson 注解JSONFieldJSONTypeJackson 应用添加 maven 依赖Jackson API序列化反序列化容器的序列…

开箱即用的密码框组件

写了一个小玩具&#xff0c;分享一下 - 组件功能&#xff1a; 初次进入页面时&#xff0c;密码隐藏显示&#xff0c;且无法查看真实密码 当修改密码时&#xff0c;触发键盘&#xff0c;输入框则会直接清空 此时输入密码&#xff0c;可以设置密码的隐藏或显示&#xff1a; …

Spark了解

目录 1 概述 2 发展 3 Spark和Hadoop 4 Spark核心模块 1 概述 Apache Spark是一个快速、通用、可扩展的分布式计算系统&#xff0c;最初由加州大学伯克利分校的AMPLab开发。 Spark可以处理大规模数据处理任务&#xff0c;包括批处理、迭代式算法、交互式查询和流处理等。Spa…

算法强化每日一题--组队竞赛

大家好 先看看题目 链接&#xff1a;组队竞赛__牛客网 [编程题]组队竞赛 牛牛举办了一次编程比赛,参加比赛的有3*n个选手,每个选手都有一个水平值a_i.现在要将这些选手进行组队,一共组成n个队伍,即每个队伍3人.牛牛发现队伍的水平值等于该队伍队员中第二高水平值。 例如: 一个队…

pytest学习和使用21-测试报告插件allure-pytest如何使用?

21-测试报告插件allure-pytest如何使用&#xff1f;1 Allure简介2 环境配置2.1 allure-pytest插件安装2.2 pytest安装2.3 allure文件下载2.4 allure环境变量配置2.5 配置java环境3 查看allure版本4 运行allure4.1 测试用例4.1 执行方法4.3 报告查看方法4.4 指定报告生成的端口4…

使用TensorFlow Serving进行模型的部署和客户端推理

目的&#xff1a;在一个server端使用TensorFlow框架对模型进行训练和保存模型文件后用TensorFlow Serving进行部署&#xff0c;使得能在客户端上传输入数据后得到server端返回的结果&#xff0c;实现远程调用的效果。环境&#xff1a;操作系统&#xff1a; ubuntu 20.04.1当然可…

函数(上)——“Python”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是Python的函数呀&#xff0c;下面&#xff0c;就让我们进入函数的世界吧 首先可以选择性地看一下小雅兰很久之前写的C语言函数章节的知识&#xff1a; 函数——“C”_认真学习的小雅兰.的博客-CSDN博客 函数递归&#xf…

【进阶数据结构】二叉搜索树经典习题讲解

&#x1f308;感谢阅读East-sunrise学习分享——[进阶数据结构]二叉搜索树 博主水平有限&#xff0c;如有差错&#xff0c;欢迎斧正&#x1f64f;感谢有你 码字不易&#xff0c;若有收获&#xff0c;期待你的点赞关注&#x1f499;我们一起进步 &#x1f308;我们在之前已经学习…

鸟哥的Linux私房菜 Shell脚本

第十二章、学习 Shell Scripts https://linux.vbird.org/linux_basic/centos7/0340bashshell-scripts.php 12.2 简单的 shell script 练习 #!/bin/bash# Program: # User inputs his first name and last name. Program shows his full name.read -p "Please in…

【SpringMVC】SpringMVC方式,向作用域对象共享数据(ModelAndView、Model、map、ModelMap)

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 向域对象共享数据一、使用 原生ServletAPI二、…

渲染机制(四):硬件加速

文章目录一、概述二、硬件绘制与软件绘制模型三、软件绘制刷新的逻辑四、总结五、参考一、概述 从 Android 3.0&#xff08;API 级别 11&#xff09;开始&#xff0c;Android 2D 渲染管道支持硬件加速&#xff0c;也就是说&#xff0c;在 View 的画布上执行的所有绘制操作都会使…

【C++】C++11新特性——可变参数模板|function|bind

文章目录一、可变参数模板1.1 可变参数的函数模板1.2 递归函数方式展开参数包1.3 逗号表达式展开参数包1.4 empalce相关接口函数二、包装器function2.1 function用法2.2 例题&#xff1a;逆波兰表达式求值2.3 验证三、绑定函数bind3.1 调整参数顺序3.2 固定绑定参数一、可变参数…

Docker入门到放弃笔记之容器

1、启动容器1.1容器hello world1.2 容器bash终端1.3 后台运行容器是 Docker 三大核心概念之一&#xff0c;其余两个是镜像与仓库。本文主讲容器。简单的说&#xff0c;容器是独立运行的一个或一组应用&#xff0c;以及它们的运行态环境。对应的&#xff0c;虚拟机可以理解为模拟…

端口镜像讲解

目录 端口类型 镜像方向 观察端口位置 端口镜像实现方式 流镜像 Vlan镜像 MAC镜像 配置端口镜像 配置本地观察端口 配置远程流镜像&#xff08;基于流镜像&#xff09; 端口镜像是指将经过指定端口的报文复制一份到另一个指定端口&#xff0c;便于业务监控和故障定位…
最新文章