此文章仅作为自己学习过程中的记录和总结,同时会有意地去用英文来做笔记,一些术语的英译不太准确,内容如有错漏也请多指教,谢谢!
一、队列(Queue)的定义:
- 队列(Queue) : 是只允许在一端进行插入操作,在另一端进行删除操作的特殊的线性表。
队列是一种先进先出(FIFO)的线性表。允许插入的一段称为队尾(an),允许删除的一端称为队头(a1)。
队列在程序设计中用的很频繁。例如,我们用键盘进行各种输入到显示器上的输出;客服系统;操作系统。
同样地,作为一种特殊的线性表,队列也有顺序、链式两种存储结构。

二、队列的抽象数据类型:
队列也有类似线性表的各种操作特性。但操作上会有些区别,特别是插入与删除操作。
Data: 性质同线性表。数据元素具有相同的类型,相邻元素具有前驱和后驱的关系。
[ADT of queue: ]
Operations :
InitQueue(*Q) : Set up an empty queue. //初始化操作,建立一个空队列Q。
DestroyQueue(*Q) : Destroy the queue if it exists. //若队列存在,则销毁它。
ClearQueue(*Q) : Empty the queue. //将队列清空。
QueueEmpty(Q) : (Bool Type) Judge whether the queue is empty. //若队列为空,则返回true,否则返回false。
GetHead(Q, *e) : If the queue exists and not empty, give the top element value to "e" and return "e". //若队列存在且非空,用e返回S的队头元素。
EnQueue(*Q, e) : If the queue exists, push new element "e" into queue to be the top. //若队列存在,插入新元素e到栈S中并成为队尾元素。
DeQueue(*Q, *e) : Delete the top element in stack, give its value to "e", and return "e". //删除队列S中的队头元素,并用e返回其值。
QueueLength(Q) : Return the amount of elements in the queue. //返回队列中元素个数。
end ADT
三、队列的顺序存储结构(Sequential Queue):
基于对队头的标定,关于这种存储结构有两种假设方法:
- 假设一个队列有n个元素,首先我们需要建立一个大于n的数组,并且将队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。易分析得,此时的入队列操作其实就是在队尾追加一个元素,不需要移动任何元素,T(n)= O(1)。但是由于队列的特性,当进行出队列操作时,所有元素都需要向前移动,以保证队头不为空,T(n) = O(n)。
- 为了消除第一种方法的低效,我们可以规定队头不一定要在下标为0的位置。为了避免当队列只有一个元素时,队头和队尾重合使得处理变得麻烦,此时我们引入两个指针:front指针指向队头元素;rear指针指向队尾元素的下一个位置。此时当 front == rear 时,此队列是空队列而不是只有一个元素。但是这种方法会存在“假溢出”的问题。即,实际上队列前端处是存在空闲位置的,但是rear指针往后加会产生数组越界的错误,无法访问到有效且空闲的位置。
由此可见,队列的顺序存储结构是存在很多缺陷的,于是我们又探寻新的更优的方法。

四、循环队列(Circular Queue):
- 循环队列(Circular Queue) : 队列头尾相接的顺序存储结构。
引入循环队列,我们可以解决队列顺序存储结构的”假溢出“问题。但是新的问题又来了:空队列时有 front == rear,而队列满时也是 front == rear。
为了解决这一问题,我们有两个办法:
- 设置一个标志变量flag,当 front == rear 且 flag == 0 时为队列空;而当 front == rear 且 flag ==1 时为队列满。
- 当队列空时,条件就是front == rear,当队列满时我们修改其条件,保留一个元素空间,也就是说当队列满时,数组中还有一个空闲单元。此时假设队列的最大尺寸为QueueSize,那么队列满的条件为:(rear + 1) % QueueSize == front。(取模 % 的目的就是为了整合rear和front大小为一个问题。)并且,通用的计算队列长度公式为:(QueueSize + rear - front) % QueueSize。

接下来是循环队列的顺序存储结构的结构代码:
[The structure code of a sequential circular queue.] //循环队列的顺序存储结构代码
typedef struct
{
QElemType data[MAXSIZE];
int front; //front指针指向队头元素。
int rear; //rear指针指向队尾元素的下一个位置。
}SqQueue;
五、循环队列部分操作具体实现:
- "InitQueue"(循环队列的初始化):
[Initialize a circular queue.] //循环队列的初始化代码
//RESULT: Initialize an empty queue.
Status InitQueue (SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
return OK;
}
- "QueueLength"(求循环队列的长度):
[Get the length of a circular queue.] //求循环队列的长度
//RESULT: Return the amount of the elements in queue "Q" (the length of it).
int QueueLength(SqQueue Q)
[
return (MAXSIZE + Q.rear - Q.front) % MAXSIZE;
]
- "EnQueue"(循环队列实现入队列操作):T(n)=O(1)
[Achieve the operation of getting a new element into the queue.] //循环队列实现入队列操作
//RESULT: If the queue isn't full, insert element "e" into "Q" as the new end-element.
Status EnQueue(SqQueue* Q, QElemType e)
{
if ((Q->rear + 1) % MAXSIZE == Q->front) //队列已满的情况。
{
return ERROR;
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXSIZE; //rear指针向后移一个位置,若到最后则转到数组头部。
return OK;
}
T(n)=O(1)
- "DeQueue"(循环队列实现出队列操作):T(n)=O(1)
[P117: Achieve the operation of moving an element away from the queue.] //循环队列实现出队列操作
//RESULT: If the queue isn't empty, delete the first element in "Q" and return its value using "e".
Status DeQueue(SqQueue* Q, QElemType* e)
{
if (Q->rear == Q->front)
{
return ERROR;
}
*e = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
return OK;
}
T(n)=O(1)

综上:可以发现单单是顺序存储,若不是循环队列则算法的时间性能是不高的; 但循环队列又面临着数组可能会溢出的问题。
六、队列的链式存储结构(linked queue):
- 链队列(linked queue):只能尾进头出的特殊的单链表。
为了操作上的方便,我们将队头指针front指向链队列的头结点,而队尾指针rear指向终端结点。空队列时,front和rear都指向头结点。

接下来是链队列的结构代码:
//链队列的结构代码:
[The structure code of a linked queue.] //The structure code of the node. 结点结构
typedef struct QNode
{
QElemType data;
struct QNode* next;
}QNode, *QueuePtr;
typedef struct
{
QueuePtr front, rear; //队列的链表结构
}LinkQueue;
七、链队列部分操作具体实现:
- "EnQueue"(循环队列实现入队列操作):T(n)=O(1)
本质上,就是在链表尾部插入新结点。
[Achieve the operation of getting a new element into the queue.] //链队列实现入队列操作
//RESULT: If the queue isn't full, insert element "e" into "Q" as the new end-element.
Status EnQueue(LinkQueue* Q, QElemType e)
{
QueuePtr newnode = (QueuePtr)malloc(sizeof(QNode));
if (!newnode)
{
exit(OVERFLOW);
}
newnode->data = e;
newnode->next = NULL;
Q->rear->next = newnode; //将新结点与原先的队尾结点连接在一起。
Q->rear = newnode; //让新结点成为新的队尾结点。
return OK;
}
T(n)=O(1)
- "DeQueue"(循环队列实现出队列操作):T(n)=O(1)
其实就是头结点的后继结点出队,将头结点的后继改为它后面的结点。若链表除了头结点外只剩一个元素,则需将rear指向头结点。
[Achieve the operation of moving an element away from the queue.] //链队列实现出队列操作
//RESULT: If the queue isn't empty, delete the first element in "Q" and return its value using "e".
Status DeQueue(LinkQueue* Q, QElemType* e)
{
QueuePtr temp;
if (Q->front == Q->rear)
{
return ERROR;
}
temp = Q->front->next;
*e = temp->data;
Q->front->next = temp->next;
if (Q->rear == temp) //若队头是队尾,则删除后将rear指向头结点。
{
Q->rear = Q->front;
}
free(temp);
return OK;
}
T(n)=O(1)

总结:
- 时间性能:循环队列与链队列的基本操作都是常数时间,即O(1)。不过循环队列是事先申请好空间,使用期间不释放;而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
- 空间性能:循环队列必须有一个固定的长度,因此就有了存储元素个数和空间浪费的问题;而链队列则不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。因此空间性能方面,链队列更加灵活。
总的来说:
在可以确定队列长度最大值的情况下,建议使用循环队列;
而如果无法预估队列的长度,则用链队列。
八、总结回顾:
栈和队列都是特殊的线性表,只不过对插入和删除操作做了限制。
- 栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
- 队列(queue)是限定只允许在一端进行插入操作,在另一端进行删除操作的线性表。
它们均可以用线性表的顺序存储结构来实现,但都存在着顺序存储结构的一些弊端,但也有对应的技巧来解决这个问题:
- 对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这样就可以最大化地利用数组的空间。
- 对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1)。
当然,它们也都可以通过链式存储结构来实现。

本文介绍了队列的基本概念,包括队列的定义和性质,以及队列的顺序存储结构(顺序队列、循环队列)和链式存储结构(链队列)。讨论了不同存储结构下的入队、出队操作及其时间复杂度,分析了循环队列解决顺序队列空间问题的方法,并对比了循环队列和链队列在时间和空间性能上的差异。最后总结了栈和队列作为特殊线性表的特点。
1198

被折叠的 条评论
为什么被折叠?



