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

    1如何理解“队列”

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

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

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

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

数据结构与算法笔记day06:队列_第1张图片

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

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

    2顺序队列和链式队列

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

        我们先用数组来实现。

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

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

数据结构与算法笔记day06:队列_第2张图片
数据结构与算法笔记day06:队列_第3张图片

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

        这个问题该如何解决呢?

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

        搬移如下图所示:

数据结构与算法笔记day06:队列_第4张图片

        数组实现的代码如下:

数据结构与算法笔记day06:队列_第5张图片
数据结构与算法笔记day06:队列_第6张图片

        运行结果:

数据结构与算法笔记day06:队列_第7张图片

        戳这里下载源代码。

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

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

        代码:

数据结构与算法笔记day06:队列_第8张图片
数据结构与算法笔记day06:队列_第9张图片
数据结构与算法笔记day06:队列_第10张图片

        运行结果:

数据结构与算法笔记day06:队列_第11张图片

        戳这里下载源代码。

    3循环队列

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

数据结构与算法笔记day06:队列_第12张图片

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

数据结构与算法笔记day06:队列_第13张图片

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

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

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

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

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

数据结构与算法笔记day06:队列_第14张图片

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

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

数据结构与算法笔记day06:队列_第15张图片
数据结构与算法笔记day06:队列_第16张图片
数据结构与算法笔记day06:队列_第17张图片

        运行结果:

数据结构与算法笔记day06:队列_第18张图片

        戳这里下载源代码。

    4阻塞队列和并发队列

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

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

数据结构与算法笔记day06:队列_第19张图片

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

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

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

    5请求线程

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

        如何存储排队的请求呢?

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

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

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

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

    内容小结

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

你可能感兴趣的:(数据结构与算法笔记day06:队列)