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

这篇具有很好参考价值的文章主要介绍了排好队,一个一个来:宫本武藏教你学队列(附各种队列源码)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:

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

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

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

理解“队列”的正确姿势

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

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

我们知道,栈只支持两个基本操作: 入栈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),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。
不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。

最后说一句

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

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

排好队,一个一个来:宫本武藏教你学队列(附各种队列源码)文章来源地址https://www.toymoban.com/news/detail-407592.html

到了这里,关于排好队,一个一个来:宫本武藏教你学队列(附各种队列源码)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 大聪明教你学Java | 深入浅出聊 ConcurrentHashMap

    🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。 🍊支持作者: 点赞👍、关注💖、留言💌~ 在 Java 的集合框架中,HashMap 是一种非常常用的数据结构,它提供了键值对形式的存储和访问方式。然

    2024年02月10日
    浏览(37)
  • 如何运用yolov5训练自己的数据(手把手教你学yolo)

    在这篇博文中,我们对YOLOv5模型进行微调,用于自定义目标检测的训练和推理。 深度学习领域在2012年开始快速发展。在那个时候,这个领域还比较独特,编写深度学习程序和软件的人要么是深度学习实践者,要么是在该领域有丰富经验的研究人员,或者是具备优秀编码技能

    2024年02月07日
    浏览(128)
  • 《手把手教你学嵌入式无人机》——入门航模遥控器使用(MC6C)

    一、 MC6C入门航模遥控器简介     六通道MC6C迈克遥控器是普遍使用的一款入门航模遥控器,价格较为低廉,同时性能比较稳定,性价比较高。 遥控器与接收机 1.基本参数: 遥控器: 遥控范围:大于800米 供电电源:4节普通5号电池 接收机: 尺寸:45*45*10(mm) 重量:9.6克 电

    2024年02月02日
    浏览(448)
  • 数据结构:图文详解 队列 | 循环队列 的各种操作(出队,入队,获取队列元素,判断队列状态)

    目录 队列的概念 队列的数据结构 队列的实现 入队 出队 获取队头元素 获取队列长度 循环队列的概念 循环队列的数据结构 循环队列的实现 判断队列是否为空 判断队列是否已满 入队 出队 得到队头元素 得到队尾元素 队列(Queue)是一种数据结构,是一种 先进先出 (First-

    2024年02月04日
    浏览(37)
  • 实现环形队列的各种基本运算的算法

    目的: 领会环形队列的存储结构和掌握环形队列中各种基本运算算法的设计。 内容: 编写一个乘成sqqueue.cpp,实现环形队列(假设栈中的元素类型ElemType为char)的各种基本运算,并在此基础上设计一个程序3-3.cpp完成以下功能。 初始化队列q。 判断队列q是否非空。 依次进队

    2024年02月06日
    浏览(32)
  • RabbitMQ之延迟队列(手把手教你学习延迟队列)

    延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列的。 1.订单在十分钟之内未支付则自动取消 2.新创建的店铺,如果在

    2024年04月17日
    浏览(41)
  • 【数据结构】带你深入栈和队列,轻松实现各种接口功能

    君兮_的个人主页 勤时当勉励 岁月不待人 C/C++ 游戏开发 Hello,米娜桑们,这里是君兮_,我们继续来学习初阶数据结构的内容,今天我们要讲的是栈与队列内容中队列部分的内容 好了,废话不多说,开始今天的学习吧! — 队列:只允许在一端进行插入数据操作,在另一端进行

    2024年02月13日
    浏览(47)
  • 码一些有用的东西网站的域名被拦截怎么办? 教你快速解除各种拦截

    今天跟大家讲解一下网站域名被拦截怎么办?怎么去解决,相信这个问题一直都是很多人的困惑吧,其实大部分行业的拦截都是可以进行处理的,针对新人来讲可能还不知道什么网站域名被拦截,下面我详细来讲解下。 什么是网站域名拦截? 网站拦截就是别人投诉了你的网

    2023年04月19日
    浏览(46)
  • SpringBoot操作ES进行各种高级查询(值得收藏),阿里P7大佬手把手教你

    for(SearchHit hit:searchHits){ // 文档的主键 String id = hit.getId(); // 源文档内容 MapString, Object sourceAsMap = hit.getSourceAsMap(); String name = (String) sourceAsMap.get(“name”); // 由于前边设置了源文档字段过虑,这时description是取不到的 String description = (String) sourceAsMap.get(“description”

    2024年04月24日
    浏览(41)
  • 数据结构之栈和队列 - 超详细的教程,手把手教你认识并运用栈和队列

    栈:后进先出 队列:先进先出 栈:是一种特殊的 线性表 , 只允许在固定的一端插入或者删除元素 ,一个栈包含了栈顶和栈底。只能在栈顶插入或者删除元素。 栈的底层 是由 数组 实现的。 栈遵循先入后出原则,也就是先插入的元素得到后面才能删除,后面插入的元素比

    2024年02月07日
    浏览(78)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包