数据结构 ☞ 队列
Last updated
Last updated
与栈一样,队列也是最基本的数据结构之一。队列也是对象的一种容器,其中对象的插入和删 除遵循“先进先出”(First-In-First-Out, FIFO)的原则⎯⎯也就是说,每次删除的只能是最先插入的 对象。因此,我们可以想象成将对象追加在队列的后端,而从其前端摘除对象。就这一性质而言, 队列与栈堪称“孪生兄弟”。比如,在银行等待接受服务时,顾客们就会排成一个队列⎯⎯最先到达 者优先得到服务。再如,用球桶来乘放羽毛球时,所有的球也排成一个队列⎯⎯你总是从一端将球 取出,而从另一端把球放入。
队列的抽象数据类型就是一个容器,其中的对象排成一个序列,我们只能访问和取出排在最前 端(Front)的对象,只能在队列的尾部(Rear)插入新对象。正是按照这一规则,才能保证最先被 插入的对象首先被删除(FIFO)。 队列ADT 首先支持下面的两个基本方法:
此外,与栈ADT 类似地,队列ADT 还支持如下的方法:
给出了从一个空队列开始,在依次执行一系列操作的过程中,队列中内容的相应变化。
队列的应用十分广泛,无论是商店、剧院、机场还是银行、医院,凡是按照“先到的客户优先 接受服务”原则的场合,都可以利用队列这一数据结构来编排相应的服务。
如下 给出了队列ADT的一个Java接口。这是一个通用的接口,来自任何类的对象,都可以 作为队列的成员。因此与栈一样,在从队列中取出一个对象后,也需要对其进行强制转换。
可以看出,这里的size()和isEmpty()方法与栈的对应方法完全相同。这两个方法加上front() 方法都属于所谓的访问式方法(Accessor method)⎯⎯这类方法仅仅读取队列的信息,但不会修 改队列的内容。
顺序数组 借助一个定长数组Q 来存放对象,即可简单地实现队列。那么,为了符合FIFO 准则,应该如 何表示和记录队列中各对象的次序呢? 一种自然的办法就是仿照栈的实现,以Q[0]作为队首,其它对象顺序往后存放。然而如此一来, 每次首元素出队之后,都需要将后续的所有元素向前顺移一个单元⎯⎯若队长为n,这项工作需要 O(n)时间,因此效率很低。
循环数组 为了避免数组的整体移动,可以引入如下两个变量fist 和last:
fist:始终等于Q 的首元素在数组中的下标,即指向下次出队元素的位置
last:始终等于Q 的末元素的下标加一,即指向下次入队元素的位置 一开始,fist = last = 0,此时队空。每次有对象入队时,将其存放于Q[last],然后r 加一,以指向下一 单元。对称地,每次有对象出队之后,也将f 加一,指向新的队首元素。这样,对front()、enqueue() 和dequeue()方法的每一次调用都只需常数时间。
然而,这还不够。细心的读者或许已经注意到,按照上述约定,在队列的生命期内,fist last 始终 在单调增加。因此,若队列数组的容量为N,则在经过N 次入队操作后,last 所指向的单元必然超出 数组的范围;在经过N 次出队操作后,fist 所指向的单元也会出现类似的问题。
解决上述问题的一种简便方法,就是在每次fist 或 last 加一后,都要以数组的长度做取模运算,以 保证其所指单元的合法性。就其效果而言,这就相当于把数组的头和尾相联,构成一个环状结构。 基于上述构想,可以得到如代码 所示的算法
请注意,这里采用的依然是定长数组,故与栈一样,有可能出现空间溢出的情况。一旦空间溢 出,需要给出ExceptionQueueFull 意外错。反过来,对于空队列的出队操作也需要禁止,这里将报 ExceptionQueueEmpty 意外错。那么,如何判断这两种情况呢?其中的一些细节值得讨论。 我们注意到,当队列中不含任何对象时,必有fist = last。然而,反之却不然。 试考虑如下情况:在数组中只剩下一个空闲单元(此时有fist ≡ (last+1) mod N)时,需要插入一个 对象。若则按照上面的enqueue()算法,插入后有fist = last,但事实上此时的队列已满。如果根据“fist = last” 判断队列为空,则尽管队列中含有元素,但出队操作却无法进行;反过来,尽管数组空间已满,却 还能插入新元素(原有的元素将被覆盖掉)。 为了解决这一问题,一种简便易行的方法就是禁止队列的实际规模超过N-1。请注意在代码 中的enqueue()算法的实现:一旦队列规模将要超过这一范围,即报ExceptionQueueFull意外错。