数据结构与算法之美——队列

如何理解队列?

就像排队买票,先来的先买,后来的人只能排在队尾,不允许插队。先进者先出,就是典型的队列。

我们知道,栈的基本操作有两个:入栈push()出栈pop(),队列的基本操作也只有两个:入队enqueue(),在队列尾部放一个数据;出队dequeue(),从队列头部去一个数据。队列和栈一样,也是一种操作受限的线性表

顺序队列和链式队列

和栈一样,队列也可以用数组来实现(顺序队列),也可以用链表来实现(链式队列)。对于栈来说,我们只需要一个栈顶指针就可以了,对于队列,需要两个指针,head指向队头(指向的是队头元素的位置),tail指向队尾(tail指向的是队尾元素的下一个空白位置)。在入队操作时,tail向后移动一个位置,出队操作时,head向后移动一个位置,我们会发现,随着不停的入队和出队操作,head和tail都会持续的向后移动,当tail移动到最末端,即便数组中有空闲空间,也无法再向队列中添加数据了,这是就需要数据搬移。但是,每次出队操作相当于删除数组中下标为0的数据,要搬移整个队列中的元素,这样出队的时间复杂度就会从原来的O(1)变成O(n)。实际上,我们在出队时可以不用搬移数据,如果没有空闲时间了,我们只需要在入队时集中触发一次数据搬移,出队函数dequeue()保持不变,对入队函数enqueue()稍加改造:当tail指针移到最末端的时候,如果有新的数据入队,将所有的数据搬移到0的位置就可以了。

基于链表实现的链式队列,同样需要两个指针,在入队时,tail->next = new_node, tail = tail->next;出队时,head = head->next。

循环队列

循环队列,顾名思义,长得像是一个环,贪吃蛇游戏结束时的蛇,在tail已经指向最后一个位置添加元素时直接添加到0的位置,通过这样的方法可以避免数据搬移,但是循环队列的代码实现难度要大很多,避免bug的最关键在于,确定好队空和队满的判定条件。假设申请的数组容量为n,队满的条件:(tail+1)%n=head。

阻塞队列和并发队列

阻塞队列其实就是在队列的基础上增加了阻塞操作。在队列为空/队列为满的时候,从队头取数据/在队尾插数据会被阻塞,直到队列有了数据/队列有了位置才能进行操作,然后再返回。(生产者-消费者模型)

在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,线程安全的队列就叫做并发队列,最简单的实现方式就是在enqueue()和dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻只允许一个存或取操作。

队列可以应用在任何有限资源池中,用于排队请求,比如线程池、数据库连接池等。基于链表的实现方式,可以实现一个无界队列,但是可能导致过多的请求排队等待,请求处理的响应时间过长。而基于数组实现的有界队列,队列的大小有限,所以线程池中的排队请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说更加合理。不过设置一个合理的队列大小,是十分讲究的,队列太大导致等待的请求太对,队列太小会导致无法充分利用系统资源、无法发挥最大性能。

猜你喜欢

转载自www.cnblogs.com/westCastle/p/10507326.html