队列 Queue
队列和栈一样,也属于受限线性表。
1 队列的基本概念
1.1 队列的定义
队列是只允许在表尾进行插入,表头进行删除的线性表。插入操作又称为入队,删除操作又称为出队。
队列的逻辑关系就和现实和生活中的队列一样,最早排队就最早离队,这种特性被称为“先入先出”(First In First Out, FIFO)。
1.2 队列的基本操作
队列的基本操作和栈类似,不同的是删除操作在队首执行。
主要操作 | 操作结果 |
---|---|
InitQueue(&Q) | 初始化一个空队列 Q。 |
DestroyQueue(&Q) | 销毁队列,并释放队列 Q 占用的存储空间。 |
ClearQueue(&Q) | 将 Q 清空为空队列。 |
QueueEmpty(Q) | 判断队列 Q 是否为空,若为空则返回 true,否则返回 false。 |
QueueLength(Q) | 返回队列 Q 的元素个数,即队列的长度。 |
GetHead(Q, &x) | 读取队首元素,若队列非空,则用 x 返回队首元素。 |
EnQueue(&Q, x) | 入队,若队列 Q 未满,则将 x 插入,使之成为新队尾。 |
DeQueue(&Q, &x) | 出队,若队列 Q 非空,则弹出队首元素,并用 x 返回。 |
2 队列的顺序表示和实现
2.1 队列的顺序表示
由于队列和栈很相似,所以自然地想到用类似顺序栈的结构来实现队列的顺序表示。队列的顺序存储通常以数组实现,并设置两个指针:头指针 front 指向队首元素,尾指针 rear 指向队尾元素的下一位。但是因为队列的首尾指针都可以移动,所以队列实现的重点是队列为空和队列为满的判断。
- 起始状态队列为空,front 和 rear 指向队首:Q.front = Q.rear = 0 。
- 入队时,先给 rear 指向的位置赋值,然后 rear 指针向后移动。
- 出队时,先保存 front 指向位置的值,然后 front 指针向后移动。
- 队列为空时,Q.front = Q.rear。
- 队列为满时,Q.rear-Q.front = MAXSIZE。
看似上述结构顺利地实现了顺序队列,但是这种实现方式其实是有问题的。假设我们现在创建了一个容量为6的队列,然后向队尾插入6个数,再从队首删除5个数,会导致 rear 指向队尾的后一位,front 指向队尾元素(如下图 (3))。此时队列未满,按理说可以继续从表尾插入元素,但是 rear 已经超过了数组的范围,所以无法继续插入。如果选择扩容,确实可以解决无法插入的问题,但是在队列未满的情况下是不应该发生扩容操作的,因此不符合逻辑。
一种可行的解决办法是将 rear 和 front 指针向下平移5个元素,使 front 重新指向数组的第一个元素。但是这种方法显然不够好,因为队列的主要操作就是删除和插入操作,这就需要频繁地进行平移操作来维护队列的结构,会增加队列操作的时间复杂度,所以我们引入另一种解决办法——循环队列。
2.2 循环队列
循环队列是通过取余运算将线性结构处理为“环形结构”的队列。循环队列并不是真的像循环链表那样,使用指针将表的首尾元素相连,而是借用了取余运算把队列从逻辑上视为一个环。
2.2.1 取余运算的作用
在循环链表中,我们将指针后移一位的操作定义为:i = (i+1) % MAXSIZE
。它和直接后移操作 i = i+1
产生的区别如下:
原位置 | 直接后移得到的新位置 | 经过取余后移得到的新位置 |
---|---|---|
0 | 1 | 1 |
1 | 2 | 2 |
2 | 3 | 3 |
3 | 4 | 4 |
4 | 5 | 5 |
5 | 6 | 0 |
可以看到,尽管实际的数组还是线性结构,取余运算却可以从队尾重新定位回队首,在逻辑上将线性结构转化为循环结构。这一特点在以后也会经常用到。
2.2.2 循环队列的特点
循环队列的示意图如下:
一般循环队列的 front 指向队首元素,rear 指向队尾元素的下一位置(循环队列中需要预留一位)
- 起始状态队列为空,front 和 rear 指向队首:Q.front = Q.rear = 0 。
- 入队时,先给 rear 指向的位置赋值,然后 rear 指针向后移动:Q.rear = (Q.rear + 1) % MAXSIZE 。
- 出队时,先保存 front 指向位置的值,然后 front 指针向后移动:Q.front = (Q.front + 1) % MAXSIZE 。
- 队列长度为:(Q.rear + MAXSIZE - Q.front) % MAXSIZE。
- 队列为空时:front = rear。
- 队列为满时:(rear+1) % MAXSIZE = front 。
在队列为空时,依旧有 front = rear,但是队列为满的情形(如上图 (4))变成了:(rear+1) % MAXSIZE = front 。如果 rear 是指向队尾元素而不是队尾元素的下一位,那么队列为满的情形就如上图 (5),也有 front = rear。这个时候仅依靠两个指针便无法判断到底是满队列还是空队列,除非使用额外的信息来帮助判断,例如队列的有效长度或其他标识。
2.3 循环队列的实现
循环队列可以用静态数组或动态数组来实现,因为静态循环队列的实现比较简单,因此不再赘述,详情见附录。这里主要介绍动态循环队列的实现。
2.3.1 动态循环队列类型定义
可以借鉴动态顺序栈:
/********** 动态循环队列的类型定义 **********/
#define INITSIZE 6
#define INCREMENT 6
typedef int ElemType;
typedef struct {
ElemType *data;
int front;
int rear;
int size; // 队列的总容量(包含预留给rear指针的空位置)
} SqQueue;
2.3.2 主要操作的实现
队列和栈一样,涉及的主要操作有8个,这里主要介绍读取队首元素操作、出队操作和入队操作,其中入队操作稍微复杂一些。
2.3.2.1 读取队首元素
/*
* Function: 读取队首元素操作
* ----------------------------
* 读取队首元素,若队列非空,则用e返回队首元素。
*/
bool GetHead(SqQueue Q, ElemType &e){
if (QueueEmpty(Q)){
return false;
}
e = Q.data[Q.front];
return true;
}
时间复杂度分析
因为队列包含队首和队尾指针,因此只需要一步就可以获取队首元素,读取队首元素操作的时间复杂度为
O
(
1
)
O(1)
O(1) 。
2.3.2.2 出队操作
/*
* Function: 出队操作
* ----------------------------
* 若队列Q非空,则弹出队首元素,并用e返回。
*/
bool DeQueue(SqQueue &Q, ElemType &e){
if (GetHead(Q, e)){
Q.front = (Q.front+1) % Q.size;
return true;
}
return false;
}
时间复杂度分析
可以借助读取队首元素操作,首先读取队首元素,然后将队首指针向后移动一位,总的时间复杂度仍为
O
(
1
)
O(1)
O(1) 。
2.3.2.3 入队操作
扩容操作可能存在的问题
动态循环队列的入队操作支持在存储空间不够使时申请内存扩容,新的扩容空间会插入动态数组的尾部,实际数组的变化如下:
而对于我们想象中的循环队列,扩容后变化如下。下图(2)红色的部分就是新加入的空间。可以看到,扩容操作成功地将队列的存储空间增加,但同时也修改了队列原有元素的逻辑关系,可能会导致队列的结构遭到破坏。例如,扩容前的循环队列中,队尾指针 rear 指向的元素(3)和队首指针 front 指向的元素(0)是相邻的。而在扩容后,新的空间被插入这两个指针之间,导致它们指向的元素不再相邻,破坏了这两个元素之间的逻辑关系。
让我们用另外一个例子来进一步说明,假设现在的队列如下图(1)所示,需要进行扩容操作。进行扩容后,得到下图(2)所示的队列。这个时候,元素
b
b
b 和
c
c
c 之间的逻辑关系被破坏。并且,尽管队列现在有多余空间,但按照原有的判满条件,该队列依然是满队列,我们无法向队列中插入任何值。因此,我们在扩容操作后还需要检查队列的原有元素关系是否遭到破坏,如被破坏需要主动维护。
维护扩容后的队列结构
那么如何维护扩容后的队列结构呢,最直观的办法就是将断裂的后半部分平移到一个符合原来逻辑关系的新位置。例如,上图中的完整队列 front->a->b->c->rear 断裂为 front->a->b 和 c->rear 两半部分。我们可以把元素
c
c
c 从位置 0 移动到位置 4,把 rear 指针从位置 1 移动到位置 5。这样,就又构成了一个完整的队列。
整个过程需要两个辅助指针 tmp1 和 tmp2,tmp1 指向后半部分队列的第一个元素,tmp2 指向前半部分队列的队尾元素的下一位。而通过观察可以发现,每一次扩容时插入操作必然发生在队列的第一个位置(不等于队首指针)和最后一个位置(不等于队尾指针)之间。因此,tmp1 和 tmp2 的位置实际上也是固定的,
- tmp1 指向数组的第一个元素,tmp1 = 0 。
- tmp2 指向扩容前数组的最后一个元素的后一位,tmp2 = Q.size 。
入队操作的实现
/*
* Function: 入队操作
* ----------------------------
* 若队列Q未满,则将e插入,使之成为新队尾,
* 否则先扩容,再插入。
*/
bool EnQueue(SqQueue &Q, ElemType e){
int oldSize = Q.size;
if (QueueLength(Q)>=Q.size-1){ // 需要扩容
Q.data = (ElemType*) realloc (Q.data, (Q.size+INCREMENT) * sizeof(ElemType));
Q.size += INCREMENT;
printf("Increment\n");
}
// 扩容完成后队列可能出现断裂
if (Q.front > Q.rear){
int tmp1 = 0; // 两个临时指针分别指向两个断裂处
int tmp2 = oldSize;
while (tmp1!=Q.rear){
Q.data[tmp2++] = Q.data[tmp1++];
}
Q.rear = tmp2;
}
// 执行插入
Q.data[Q.rear] = e;
Q.rear = (Q.rear+1) % Q.size;
return true;
}
时间复杂度分析
- 最优情况:无需扩容,直接插入,时间复杂度为 O ( 1 ) O(1) O(1) 。
- 最差情况:先扩容,然后维护队列结构,最后再执行插入操作。假设 realloc 函数重新分配空间并且需要复制元素,那么扩容操作需要时间 O ( n ) O(n) O(n) ;维护队列结构最多需要移动 n n n 次,时间复杂度为 O ( n ) O(n) O(n) ;插入操作需要一步,时间复杂度为 O ( 1 ) O(1) O(1)。因此,总的时间复杂度为 O ( n ) O(n) O(n)。
3 队列的链式表示和实现
3.1 队列的链式表示
队列的链式表示称为链队列,是一个同时带有头指针和尾指针的单链表,头指针指向头结点,尾指针指向队尾结点。链队列的大部分操作和单链表一致,只是插入操作只能在表尾进行,删除操作只能在表头进行。
- 在队列为空时,只有头结点,因此 Q.front = Q.rear。
- 入队时,将新结点加入链表尾部,然后将尾指针后移。若原队列为空,则头指针也需移动。
- 出队时,若队列非空,则删除第一个结点,头指针后移。
- 求队列长度需要遍历链表,从头指针出发,到尾指针结束。
- 队列为空的判断条件是两个指针重合,Q.front = Q.rear。
链队列适合数据元素变动较大的情形,且不存在队列满导致溢出的问题。
3.2 链队列的实现
3.2.1 链队列的类型定义
先定义结点类,然后定义链队列。链队列至少要包含两个指针,还可以储存如队列的长度这样的辅助信息。
/********** 链队列的类型定义 **********/
typedef int ElemType;
typedef struct LNode{
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode;
typedef struct {
LNode *front, *rear;
} LinkedQueue;
3.2.2 主要操作的实现
链队列大部分操作的时间复杂度和单链表一致,但是因为保留了尾指针,所以链队列的尾部插入操作时间复杂度仅为 O ( 1 ) O(1) O(1) 。实现详情见附录。
4 双端队列
除了栈和队列外,还有一种受限线性表叫双端队列(Double End Queue,也叫 Deque)。其特点是队列的两端都可以执行插入或删除操作,两端分别叫做端点 1 和端点 2。在实际使用中,还有输出受限的双端队列和输入受限的双端队列:
- 输出受限的双端队列:两端都可以输入,但只有一端可以输出。
- 输入受限的双端队列:两端都可以输出,但只有一端可以输入。
由于双端队列在实际应用中并不实用,所以仅列出了解。例题见此处
相关章节
第一节 【绪论】数据结构的基本概念
第二节 【绪论】算法和算法评价
第三节 【线性表】线性表概述
第四节 【线性表】线性表的顺序表示和实现
第五节 【线性表】线性表的链式表示和实现
第六节 【线性表】双向链表、循环链表和静态链表
第七节 【栈和队列】栈
第八节 【栈和队列】栈的应用
第九节 【栈和队列】栈和递归
第十节 【栈和队列】队列
附录
队列的顺序实现
静态循环队列
/*
* Filename:StaticSqQueue.h
* -----------------------
* 静态数组实现循环队列。
*/
#ifndef _STATIC_SEQUENTIAL_QUEUE_h_
#define _STATIC_SEQUENTIAL_QUEUE_h_
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
/********** 循环队列的类型定义 **********/
#define MAXSIZE 6
typedef int ElemType;
typedef struct {
ElemType data[MAXSIZE];
int front;
int rear;
} SqQueue;
/********** 循环队列主要操作的实现 **********/
/*
* Function: 初始化操作
* ----------------------------
* 初始化一个空队列Q。
*/
void InitQueue(SqQueue &Q){
Q.front = 0;
Q.rear = 0;
}
/*
* Function: 销毁队列操作
* ----------------------------
* 静态数组的销毁不需要手动执行,因此无需销毁操作。
*/
void DestroyQueue(SqQueue &Q){}
/*
* Function: 清空队列操作
* ----------------------------
* 将Q清为空队列。
*/
void ClearQueue(SqQueue &Q){
Q.rear = Q.front; // 将队列设置为空
}
/*
* Function: 判空操作
* ----------------------------
* 判断队列Q是否为空,若为空则返回true,否则返回false。
*/
bool QueueEmpty(SqQueue Q){
return Q.front==Q.rear;
}
/*
* Function: 求队长操作
* ----------------------------
* 返回队列Q的元素个数,即队列的长度。
*/
int QueueLength(SqQueue Q){
return ((Q.rear+MAXSIZE-Q.front) % MAXSIZE);
}
/*
* Function: 读取队首元素操作
* ----------------------------
* 读取队首元素,若队列非空,则用e返回队首元素。
*/
bool GetHead(SqQueue Q, ElemType &e){
if (QueueEmpty(Q)){
return false;
}
e = Q.data[Q.front];
return true;
}
/*
* Function: 入队操作
* ----------------------------
* 若队列Q未满,则将e插入,使之成为新队尾。
*/
bool EnQueue(SqQueue &Q, ElemType e){
if (QueueLength(Q)<MAXSIZE-1){ // 注意要预留一个空位置
Q.data[Q.rear] = e;
Q.rear = (Q.rear+1) % MAXSIZE;
return true;
} else {
printf("Out of space!\n");
return false;
}
}
/*
* Function: 出队操作
* ----------------------------
* 若队列Q非空,则弹出队首元素,并用e返回。
*/
bool DeQueue(SqQueue &Q, ElemType &e){
if (GetHead(Q, e)){
Q.front = (Q.front+1) % MAXSIZE;
return true;
}
return false;
}
/*
* Function: 输出操作
* ----------------------------
* 按从队首头到队尾的顺序输出。
*/
void Print(SqQueue Q){
int len = QueueLength(Q);
for (int i=Q.front;i<Q.front+len;i++){
printf("%d <-", Q.data[i]);
}
printf("\n");
}
#endif // _STATIC_SEQUENTIAL_QUEUE_h_
动态循环队列
/*
* Filename: DynamicSqQueue.h
* -----------------------
* 使用动态数组实现循环队列。
*/
#ifndef _DYNAMIC_SEQUENTIAL_QUEUE_h_
#define _DYNAMIC_SEQUENTIAL_QUEUE_h_
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
/********** 动态循环队列的类型定义 **********/
#define INITSIZE 6
#define INCREMENT 6
typedef int ElemType;
typedef struct {
ElemType *data;
int front;
int rear;
int size; // 队列的总容量(包含预留给rear指针的空位置)
} SqQueue;
/********** 动态循环队列主要操作的实现 **********/
/*
* Function: 初始化操作
* ----------------------------
* 初始化一个空队列Q。
*/
void InitQueue(SqQueue &Q){
Q.data = new ElemType[INITSIZE];
Q.front = 0;
Q.rear = 0;
Q.size = INITSIZE;
}
/*
* Function: 销毁队列操作
* ----------------------------
* 销毁队列,并释放队列Q占用的存储空间。
*/
void DestroyQueue(SqQueue &Q){
Q.rear = 0;
Q.front = 0;
delete[] Q.data;
}
/*
* Function: 清空队列操作
* ----------------------------
* 将Q清为空队列。
*/
void ClearQueue(SqQueue &Q){
Q.rear = Q.front; // 将队列设置为空
}
/*
* Function: 判空操作
* ----------------------------
* 判断队列Q是否为空,若为空则返回true,否则返回false。
*/
bool QueueEmpty(SqQueue Q){
return Q.front==Q.rear;
}
/*
* Function: 求队长操作
* ----------------------------
* 返回队列Q的元素个数,即队列的长度。
*/
int QueueLength(SqQueue Q){
return ((Q.rear+Q.size-Q.front) % Q.size);
}
/*
* Function: 读取队首元素操作
* ----------------------------
* 读取队首元素,若队列非空,则用e返回队首元素。
*/
bool GetHead(SqQueue Q, ElemType &e){
if (QueueEmpty(Q)){
return false;
}
e = Q.data[Q.front];
return true;
}
/*
* Function: 入队操作
* ----------------------------
* 若队列Q未满,则将e插入,使之成为新队尾,
* 否则先扩容,再插入。
*/
bool EnQueue(SqQueue &Q, ElemType e){
int oldSize = Q.size;
if (QueueLength(Q)>=Q.size-1){ // 需要扩容
Q.data = (ElemType*) realloc (Q.data, (Q.size+INCREMENT) * sizeof(ElemType));
Q.size += INCREMENT;
printf("Increment\n");
}
// 扩容完成后队列可能出现断裂
if (Q.front > Q.rear){
int tmp1 = 0; // 两个临时指针分别指向两个断裂处
int tmp2 = oldSize;
while (tmp1!=Q.rear){
Q.data[tmp2++] = Q.data[tmp1++];
}
Q.rear = tmp2;
}
// 执行插入
Q.data[Q.rear] = e;
Q.rear = (Q.rear+1) % Q.size;
return true;
}
/*
* Function: 出队操作
* ----------------------------
* 若队列Q非空,则弹出队首元素,并用e返回。
*/
bool DeQueue(SqQueue &Q, ElemType &e){
if (GetHead(Q, e)){
Q.front = (Q.front+1) % Q.size;
return true;
}
return false;
}
/*
* Function: 输出操作
* ----------------------------
* 按从队首头到队尾的顺序输出。
*/
void Print(SqQueue Q){
int len = QueueLength(Q);
for (int i=Q.front;i<Q.front+len;i++){
printf("%d <-", Q.data[i]);
}
printf("\n");
}
#endif // _DYNAMIC_SEQUENTIAL_QUEUE_h_
循环队列检测程序
/*
* Filename: SqQueueTest.cpp
* -----------------------
* 检测循环队列。
*/
// 引用静态循环队列
#include "StaticSqQueue.h"
// 引用动态循环队列
// #include "DynamicSqQueue.h"
int main(){
SqQueue Q;
InitQueue(Q);
int n;
ElemType e;
char helpInfo[] =
"*****************************\n"
"Sequential Queue check: \n"
"\t-2-Quit\n"
"\t1-EnQueue\n"
"\t2-DeQueue\n"
"\t3-Empty check\n"
"\t4-Get Length\n"
"\t5-Get head\n"
"\t6-Clear\n"
"\t7-Print\n"
"*****************************\n";
while (n!=-2){
printf(helpInfo);
scanf("%d", &n);
switch(n){
case 1:
printf("Enter the new value: ");
scanf("%d", &e);
EnQueue(Q, e);
break;
case 2:
if (DeQueue(Q, e)){
printf("The first value %d is dequeued.\n", e);
} else {
printf("The Queue is empty.\n");
}
break;
case 3:
if (QueueEmpty(Q)){
printf("The Queue is empty.\n");
} else {
printf("The Queue is not empty.\n");
}
break;
case 4:
printf("The length of Queue is: %d\n", QueueLength(Q));
break;
case 5:
if (GetHead(Q, e)){
printf("The Head value is: %d\n", e);
} else {
printf("The Queue is empty.\n");
}
break;
case 6:
ClearQueue(Q);
printf("All cleared.\n");
break;
case 7:
printf("Queue is: ");
Print(Q);
break;
}
}
DestroyQueue(Q);
return 0;
}
单向链队列
单向链队列的实现
/*
* Filename: SingleLinkedQueue.h
* -----------------------
* 使用单链表实现队列。
*/
#ifndef _SIGNLE_LINKED_LIST_h_
#define _SIGNLE_LINKED_LIST_h_
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
/********** 链队列的类型定义 **********/
typedef int ElemType;
typedef struct LNode{
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode;
typedef struct {
LNode *front, *rear;
} LinkedQueue;
/********** 链队列主要操作的实现 **********/
/*
* Function: 初始化操作
* ----------------------------
* 初始化一个空队列Q。
*/
void InitQueue(LinkedQueue &Q){
Q.front = new LNode; // 头结点
Q.front->next = NULL;
Q.rear = Q.front;
}
/*
* Function: 清空队列操作
* ----------------------------
* 将Q清为空队列。
*/
void ClearQueue(LinkedQueue &Q){
LNode *tmp = Q.front->next;
while (tmp!=NULL){
Q.front->next = tmp->next;
delete tmp;
tmp = Q.front->next;
}
Q.rear = Q.front;
}
/*
* Function: 销毁队列操作
* ----------------------------
* 销毁队列,并释放队列Q占用的存储空间。
*/
void DestroyQueue(LinkedQueue &Q){
ClearQueue(Q);
delete Q.front;
}
/*
* Function: 判空操作
* ----------------------------
* 判断队列Q是否为空,若为空则返回true,否则返回false。
*/
bool QueueEmpty(LinkedQueue Q){
return Q.front==Q.rear;
}
/*
* Function: 求队长操作
* ----------------------------
* 返回队列Q的元素个数,即队列的长度。
*/
int QueueLength(LinkedQueue Q){
int count=0;
LNode *tmp = Q.front->next;
while (tmp!=NULL){
count++;
tmp = tmp->next;
}
return count;
}
/*
* Function: 读取队首元素操作
* ----------------------------
* 读取队首元素,若队列非空,则用e返回队首元素。
*/
bool GetHead(LinkedQueue Q, ElemType &e){
if (QueueEmpty(Q)){
return false;
}
e = Q.front->next->data;
return true;
}
/*
* Function: 入队操作
* ----------------------------
* 将e插入,使之成为新队尾,
* 注意插入完成后要更新尾指针。
*/
bool EnQueue(LinkedQueue &Q, ElemType e){
LNode *n = new LNode; // 创建新的队尾结点
n->data = e;
n->next = NULL;
Q.rear->next = n; // 将n加入队尾
Q.rear = n; // 更新尾指针
return true;
}
/*
* Function: 出队操作
* ----------------------------
* 若队列Q非空,则弹出队首元素,并用e返回。
*/
bool DeQueue(LinkedQueue &Q, ElemType &e){
if (QueueEmpty(Q)){
return false;
}
LNode *tmp = Q.front->next;
e = tmp->data;
if (tmp==Q.rear){ // 如果队列只含有一个元素,删除后rear指针也会丢失
Q.rear=Q.front; // 因此可以提前将rear指向front
}
Q.front->next = tmp->next;
delete tmp;
return true;
}
/*
* Function: 输出操作
* ----------------------------
* 按从队首头到队尾的顺序输出。
*/
void Print(LinkedQueue Q){
LNode *tmp = Q.front->next;
while (tmp!=NULL){
printf("%d ->", tmp->data);
tmp = tmp->next;
}
printf("\n");
}
#endif // _SIGNLE_LINKED_LIST_h_