队列ADT
像栈一样,队列(queue)也是表。然而,使用队列时插入在一端进行而删除则在另一端进行。
队列模型
队列的基本操作是(入队),它是在表的末端(叫做队尾(rear))插入一个元素,还有(出队),它是删除(或返回)在表的开头(叫做队头(front))的元素。
队列的数组实现
如同栈的情形一样,对于队列而言任何表的实现都是合法的。像栈一样,对于每一种操作,链表实现和数组实现都给出快速的运行时间。队列的链表实现是直接的。
对于每一个队列数据结构,我们保留一个数组以及位置和,它们代表队列的两端。我们还要记录实际存在队列中的元素的个数。所有这些信息是作为一个结构的一部分,除队列例程本身外通常不会有例程直接访问它们。下图表示处于某个中间状态的一个队列。顺便指出,图中那些空白单元是有着不确定的值的。特别地,前三个单元含有曾经属于该队列的元素。
为使一个元素入队,我们让和增1,然后置。若使一个元素出队,我们置返回值为,减1,然后使增1。现在论述错误的检测。
这种实现存在一个潜在的问题。经过10次入队后队列似乎是满了,因为现在是10,而下一次再入队就会是一个不存在的位置。然而,队列中也许只存在几个元素,因为若干元素可能已经出队了。像栈一样,即使在有许多操作的情况下队列也常常不是很大。
简单的解决方法是,只要或到达数组的尾端,它就又绕回到开头。下图显示在某些操作期间的队列情况。这叫做***循环数组(circular array)***实现。
实现回绕所需要的附加代码是极小的(虽然它可能使得运行时间加倍)。如果或增1使得超越了数组,那么其值就要重置为数组的第一个位置。
关于队列的循环实现,有两件事情要警惕。第一,检测队列是否为空是很重要的,因为当队列 为空时一次操作将不知不觉地返回一个不确定的值。第二,某些程序设计人员使用不同的方法来表示队列的队头和队尾。例如,有些人并不用一个单元来表示队列的大小,因为他们依靠的是基准情形,即当队列为空时,。队列的大小是通过比较和隐式算出的。这是一种非常隐秘的方法,因为存在某些特殊情形,因此,如果你需要修改用这种方式编写的代码,那么你就要特别仔细。如果队列的大小不是结构的一部分,那么若数组的大小为,则当存在个元素时队列就满了,因为只有个不同的大小值可被区分,而0是其中的一个。
首先,下面给出了队列的声明。
#ifndef _Queue_h
#define _Queue_h
typedef int ElementType;
struct QueueRecord;
typedef struct QueueRecord *Queue;
int IsEmpty(Queue Q);
int IsFull(Queue Q);
Queue CreateQueue(int MaxElements);
void DisposeQueue(Queue Q);
void MakeEmpty(Queue Q);
void Enqueue(ElementType X, Queue Q);
ElementType Front(Queue Q);
void Dequeue(Queue Q);
ElementType FrontAndDequeue(Queue Q);
#endif /* _Queue_h */
/* Place in implementation file */
/* Queue implementation is a dynamically allocated array */
#define MinQueueSize (5)
struct QueueRecord
{
int Capacity;
int Front;
int Rear;
int Size;
ElementType *Array;
};
测试一个队列是否为空的例程以及构造一个空队列的例程。
int IsEmpty(Queue Q)
{
return Q->Size == 0;
}
void MakeEmpty(Queue Q)
{
Q->Size = 0;
Q->Front = 1;
Q->Rear = 0;
}
下面是入队的例程。
int Succ(int Value, Queue Q)
{
if (++Value == Q->Capacity)
Value = 0;
return Value;
}
void Enqueue(ElementType X, Queue Q)
{
if (IsFUll(Q))
printf("Full queue");
else
{
Q->Size++;
Q->Rear = Succ(Q->Rear, Q);
Q->Array[Q->Rear] = X;
}
}
注意,在之前与初始化为1。
队列的应用
有几种使用队列给出提高运行效率的算法。它们当中有些可以在图论中找到,这里,先给出某些应用队列的例子。
当作也送交给一台行式打印机,它们就按照到达的顺序被排列起来。因此,被送往行式打印机的作业基本上是被放到一个队列中。
实际生活中的每次排队都(应该)是一个队列。例如,在一些售票口排列的队都是队列,因为服务的顺序是先来到的先买票。
另一个例子是关于计算机网络的。有许多种PC机的网络设置,其中磁盘是放在一台叫做文件服务器的机器上的。使用其他计算机的用户是按照先到先使用的原则访问文件的,因此其数据结构是一个队列。
进一步的例子如下:
- 当所有的接线员忙得不可开交的时候,对大公司的传呼一般都被放到一个队列中。
- 在大规模的大学里,如果所有的终端都被占用,由于资源有限,学生们必须在一个等待表上签字。在终端上呆的时间最长的学生将首先被强制离开,而等待时间最长的学生将是下一个被允许使用终端的用户。
处理用概率的方法计算用户排队预计等待时间、等待服务的队列能够排多长,以及其他一些诸如此类的问题将用到被称为*排队论(queueing theory)*的整个数学分支。问题的答案依赖于用户加入队列的频率以及一旦用户的到服务时处理服务花费的时间。这两个参数作为概率分布函数给出。一种简单的例子是一条电话线有一个接线员。如果接线员忙,打来的电话就被放到一个等待队列中(这还与某个容许的最大限度有关)。
如果我们有个接线员,那么这个问题解决起来要困难得多。解析求解困难的问题往往使用模拟的方法求解。此时,我们需要一个队列来进行模拟。如果很大,那么我们还需要其他一些数据结构来使得模拟更有效地进行。
正如栈一样,队列还有其他丰富的用途,这样一种简单的数据结构竟然能够如此重要,实在令人惊奇。