美文网首页
数据结构与算法笔记day06:队列

数据结构与算法笔记day06:队列

作者: 楠楠喜欢泡枸杞 | 来源:发表于2019-04-29 17:16 被阅读0次

        1如何理解“队列”

            先进者先出,这就是典型的“队列”。

            队列跟栈十分相似,最基本的操作也只有两个:

            入队:放一个元素到队列尾部。

            出队:从队列头部取一个元素。

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

            作为一种非常基础的数据结构,它的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。

        2顺序队列和链式队列

            跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的队列叫做顺序队列,用链表实现的队列叫做链式队列

            我们先用数组来实现。

            它需要两个指针:头指针head和为指针tail。

            简单的出队操作如下图所示(出队略,自行脑补,哈哈):

            随着不停地进行入队、出队操作,head和tail都会持续往后移动,当tail移动到最右边,即使数组中海油空闲空间,也无法继续往队列中添加数据了。

            这个问题该如何解决呢?

            很简单,在入队时稍微加一个判断就好了(出队不用判断哦)。当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将head到tail之间的数据,整体搬移到数组中0到tail-head的位置。因为只有在tail指针移动到数组最右边时才会进行一次整体的搬移,所以时间复杂度平摊下来依然是O(1)。(如果每次入队都搬移的话,时间复杂度就会从原来的O(1)变成O(n))

            搬移如下图所示:

            数组实现的代码如下:

            运行结果:

            戳这里下载源代码。

            接下来用链表实现,同样需要两个指针head和tail。

            在实现的过程中,我发现自己之前写代码的一个问题,就是入队出队功能都写在主函数中并没有封装,这样复用性就太差了。从这次链表的实现开始,我要优化自己的代码,以后将功能都封装在类中~

            代码:

            运行结果:

            戳这里下载源代码。

        3循环队列

            循环队列,顾名思义,它长得像一个环。队列原本是有头有尾的一条直线,现在我们把它的首尾相连,连成了一个环,如下图所示。

            上图中队列的大小为8,当前head=4,tail=7。此时一个新元素a入队,将会被放到下标为7的位置,tail更新成了0而不是8。当又有一个新元素b入队时,将它放到下标为0的位置,tail更新成了1。如下图所示:

            循环队列的好处是,避免了数据搬移操作。

            下面用代码来实现它。实现循环队列的两个关键点在于:

            1,确定队空条件:head==tail。

            2,确定队满条件:(tail+1)%n=head。

            如下图所示,是队满的一种情况:

            我们发现,tail指向的位置实际上是没有存储数据的,所以循环队列会浪费一个数组的存储空间。(如果把这个也存了,就无法判断循环队列的队空与队满)

            我写的代码如下(用数组实现):

            运行结果:

            戳这里下载源代码。

        4阻塞队列和并发队列

            实际上,队列这种数据结构很基础,平时的业务开发基本都不会直接用到。而一些具有特殊特性的队列应用却比较广泛,比如阻塞队列和并发队列。

            阻塞队列其实就是在队列基础上增加了阻塞操作。在队列为空的时候,从队头取数据会被阻塞,因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

            不难看出,上述的定义就是一个“生产者-消费者模型”。我们可以用阻塞队列,轻松实现一个生产者消费者模型!

            这种基于阻塞队列实现的“生产者-消费者模型”,可以有效地协调生产和消费的速度。当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满了。这个时候生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。

            而且基于阻塞队列,我们还可以通过协调“生产者”和“消费者”个数,来提高数据的处理效率。比如前面的例子,我们可以多配置几个“消费者”,来应对一个“生产者”。但是在多线程情况下,多个线程同时操作队列,会存在安全问题。而基于数组的循环队列,利用CAS原子操作,可以实现非常高效出现的并发队列。这也是循环队列比链式队列应用更加广泛的原因,后面会详细说并发队列的应用~(其实最简单直接的方式是在入队和出队操作上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。)

        5请求线程

            当线程池中没有空闲线程,新的任务请求线程资源时,我们一般有两种处理策略:1)非阻塞的处理方式,直接拒绝任务请求;2)阻塞的处理方式,将请求排队,等有空闲线程时,取出排队的请求继续处理。

            如何存储排队的请求呢?

            为了公平起见,我们肯定要先进先出,所以队列这种数据结构很适合存储排队请求。

            那我们该用数组还是链表来实现呢?

            用链表可以实现一个支持无线排队的无界队列,但是可能导致排队的请求过多,请求处理的相应时间也过长,所以针对响应时间比较敏感的系统,基于链表实现的无线排队的线程池是不合适的。

            用数组可以实现一个大小有限的队列,这样当线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝。这样的方式对响应时间敏感的系统来说比较合适。但是要注意哦,设置一个合理的队列大小也是非常重要的,队列太大会导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。

        内容小结

            队列最大的特点就是先进先出,主要的两个操作是入队和出队。它既可以用数组实现(顺序队列),也可以用链表实现(链式队列)。用数组实现队列会有数据搬移操作,而若用数组实现循环队列则避免了数据搬移的操作。循环队列是重点,代码实现的关键是确定好队空和队满的判定条件。除此之外,还学习了集中高级的队列结构,阻塞队列、并发队列,底层都还是用队列这种数据结构,但是在它之上附加了很多其他功能。阻塞队列就是入队、出队操作可以阻塞,并发队列要注意队列的操作多线程安全。

    相关文章

      网友评论

          本文标题:数据结构与算法笔记day06:队列

          本文链接:https://www.haomeiwen.com/subject/oqcsnqtx.html