数据结构——线性结构(4)——顺序队列与循环队列的原理

队列的接口

从上一个专栏可以看出,栈和队列是非常相似的结构。它们之间的唯一区别是处理元素的顺序。栈使用后进先出(LIFO)的规律,其中对于来说push的最后一个元素始终是第一个pop的元素。而队列采用更接近于排队的的先进先出(FIFO)模式。栈和队列的接口也非常相似。两个接口的public部分的唯一变化是定义类的行为的两种方法的名称。 来自Stack类的push方法现在称为入队(enqueue),pop方法现在称为出队(dequeue)。这些方法的行为也不同。
鉴于这些结构及其接口的概念相似性,使用基于数组或基于链表的策略都可以实现栈和队列。然而,对于这些模型中的每一个,队列的实现具有在栈的情况下不会出现的细微之处。这些差异起因于栈上的所有操作都发生在内部数据结构的同一端。在队列中,排队操作发生在一端,出队操作发生在另一端。我们可以翻到以前的博客中看看两者的区别图解:
C++抽象编程——STL(3)——queue 类

基于数组实现的队列

鉴于队列中的动作不再局限于队列的一端,所以我们需要两个索引来跟踪队列中的头和尾位置。 因此私有实例变量看起来像这样:

ValueType *array;
int capacity;
int head;
int tail;

在该表示中,head成员保存下一个将要出队列的元素的索引,tail成员下一个将要入队的元素的索引。 在一个空的队列中,很明显,tail成员应该为0,以指示数组中的初始位置,但head成员呢? 为了方便起见,通常的策略是将head设置为0。当以这种方式定义队列时,使head和tail成员相等表示队列为空。即队空等价于:

head == tail;

由此,我们很容易得出,我们初始化一个空队列(也就是我们的构造函数)应该是这样的:

template <typename ValueType>
Queue<ValueType>::Queue(){
    head = tail = 0;
}

虽然令人兴奋的是认为入队和出队方法看起来几乎完全像Stack类中的push和pop对应,但如果你只是尝试复制现有代码,那么你将遇到好几个问题。 通常在编程中,我们通过绘制图表可以确保在转向实现之前,你必须准确了解队列的运行方式。
要了解队列的这种表示方法的工作原理,可以想象队列代表一个等待线,类似于我们之前模拟离散事件中的一个等待线。一个新的客户不定期到达并添加到队列中。等待排队的客户定期在队列的前端提供服务,之后他们完全离开等候线。队列数据结构如何响应这些操作?
C++抽象编程——STL(3)——离散事件模拟与排队问题
假设一开始的队列为空队列,那么内部的结构应该是这样的:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第1张图片
假设现在有五个客户到达,以字母A到E表示。这些客户按顺序排队,从而产生以下配置:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第2张图片
head域中的值0指示队列中的第一个客户存储在数组的位置0; tail的值5表示下一个客户将被放置在位置5,很好。此刻假设你在队列开始时交替地为客户服务,然后添加一个新的客户到最后。例如,客户A出站,客户F到达,导致以下情况:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第3张图片
假设你在下一个客户到达之前继续为一位客户提供服务,并且直到客户J到达。队列的内部结构如下所示:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第4张图片
在这一点上,你有一个问题。此时,队列中只有五个客户,但你已经占用了所有可用空间。tail指向超出数组的末尾。另一方面,你现在在数组的开头有未使用的空间。因此,如果你代替增加的尾部,使其表示不存在的位置10,您可以从数列的末端“环绕”回到位置0,如下所示:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第5张图片
此时,你就有空间将客户K排入位置0,这将导致以下配置:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第6张图片
如果允许队列中的元素从数组结尾开始循环,则活动元素始终从head索引向上延伸到tail索引之前的位置,如图所示:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第7张图片
因为数组的首末端就像是连接在一起,程序员把这个表示称为一个环形缓冲区(ring buffer)。

在你编写enqueue和dequeue代码之前,你需要考虑的唯一剩余问题是如何检查队列是否已满。测试完整队列比你想象的更复杂。要了解可能出现问题的原因,假设有三名客户随后到达。假设你排队客户L,M和N,显然都在K的后面,数据结构如下所示:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第8张图片
在这一点上,似乎有一个额外的空间。 不过,如果客户O现在到达会发生什么? 如果你遵循较早的入队操作的逻辑,则最终导致以下配置中:
数据结构——线性结构(4)——顺序队列与循环队列的原理_第9张图片
队列中的数组现在已经完全满了。不幸的是,此时head跟tail具有相同的值,就像在这个图中一样,队列被认为是空的。没有办法从队列结构本身的内容中得出两个条件中的哪一个是空的或全满的,因为每种情况下的数据值看起来都一样。虽然你可以通过对空队列采用不同的定义并编写一些特殊情况代码来解决此问题,但最简单的方法是将队列中的元素数量限制为小于容量的数量,并在每次扩展数组限制它到达尾部。

队列类模板的环形缓冲区实现代码我们下篇博客再写上。值得注意的是,代码没有显式测试数组索引,以查看它们是否从数组结尾开始循环。相反,代码使用%运算符自动计算正确的索引。使用余数将计算结果减少到小的周期性整数范围的技术是称为模数运算,来段英文解释(The technique of using remainders to reduce the result of a computation to a small, cyclical range of integers is an important mathematical technique called modular arithmetic.

你可能感兴趣的:(数据结构与算法深入)