队列

队列的定义

定义

队列是只允许在一端进行插入操作,而在另一端进行删除操纵的特殊的线性表。

允许进行元素插入操作的一端称为队尾(rear),允许进行元素删除操作的一端称为队头(front)。在队尾插入元素的操作称为入队(Enqueue),在队头删除元素的操作称为出队(Dequeue)如图1-1-1所示。

特点

先进先出(First In First Out)

#队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,输出数据只能在队头进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
ADT
队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有线性关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(*Q):若队列Q为空,则返回true,否则返回false。
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e):删除队列Q中队头元素并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。

循环队列

栈作为一种特殊的线性表,栈有链表栈和顺序栈两种实现方式。同样对于队列来说,队列也是一种特殊的线性表,因此也同样有两种实现方式,但队列更为特殊一点,实现队列常用的两种方式分别是循环队列和链式队列。

顺序队列

在引入循环队列之前,我们先来分析一下队列顺序存储的不足。

和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队头到队尾的元素之外,还需要两个指针进行管理。一个是队头指针(front)指向队头元素,另一个是队尾指针(rear)指向下一个入队元素的存储位置。

当我们需要向队列添加一个元素时,只要在队尾追加一个元素就可以了,不需要移动任何元素,因此时间复杂度为θ(1),如图3-1-1所示。

当我们需要队头的元素出队时,第一个位置就空出来了。那就意味着,队列中剩下的所有元素都得向前移动,以保证队列的队头有元素存放(如图3-1-2所示)。这就好比我们排队买票,前面的人买好了离开,后面的人就要向前不缺空位,这似乎很合理。但在队列中,若要完成这一行为,此时的时间复杂度为θ(n),显然这中行为是浪费的。

既然补缺空位的做法是浪费的,那何必出队列时一定要全部移动呢。如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加。也就是说,队头(front)不需要一定在存储空间的最前面,如图3-1-3所示。

但是,当我们这样定义的时候,会出现一种比较的特殊的情况,如图3-1-4所示。

正如图3-1-4所示,A5入队之后,rear指针移动了数组之外,数组之外是哪里呢?是不是出错了?如果再有一个A6入队,因为队列添加元素只能在队尾进行操作,但数组末尾已经备注占用了,再向后加产生了错误。是不是就不能添加元素了呢?如果你上了俩公交车,发现前两排有个空座位,而后排所有座位都已经占满了,那你会说车上已经没有座位了,所以你要下车。显然这是不可能的。

在队列中若出现的这种情况,我们称为“假上溢”现象,除此之外,还有“真上溢”,“下溢”现象。

“假上溢”现象:由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为”假上溢”现象。

“真上溢”现象:当队列满时,做进栈运算产生空间溢出的现象。“真上溢”是一种出错状态,应设法避免。

“下溢”现象:当队列为空时,做出队运算产生的溢出现象。“下溢”是正常现象,常用作程序控制转移的条件。

“假上溢”现象显然是很浪费的一种情况,我们肯定是要想办法解决这一问题的,随之而来的也就引入了循环队列。

循环队列

很简单,假上溢的时候,既然后面的满了,前面的还有空缺,那就从头在开始呗,也就是头尾想接的循环。我们把队列的这种头尾相接的顺序储存结构称为循环队列。在图3-1-4中,我们就可以根据循环队列的定义将rear改为下标为0的位置,如图3-2-1所示。

事实上,我们换一种方式更容易理解循环队列,如图3-2-1所示。

若我们这样定义循环队列时,我们会发现还有问题:
当队列没有元素,为空队列,此时rear=front,如图3-2-3所示;

当队列满时,此时rear=front,如图3-2-4所示。

也就是说当rear=front时,我们并不能确定此时队列究竟是空队列还是满队列。此时有两种方法解决这一问题:

  1. 设置一个标志变量flag,,当front=rear,且flag=0时为空队列,当front=rear。且flag=1是为满队列。
  2. 少用一个空间,也就是说当数组中还有一个空闲单元时,我们认为队列已经满了。如图3-2-5所示。而当队列空时,条件就是front=rear,即我们不允许图3-2-4所示的情况出现。

除了上面我们分析的一些情况,在具体的设计中,必须为循环队列设定一个最大队列的长度。具体的代码可参考下面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/* 队列的顺序存储结构(循环队列) */
#define MAX_QSIZE 5 /* 最大队列长度+1 */
typedef struct
{
QElemType *base; /* 初始化的动态分配存储空间 */
int front; /* 头指针,若队列不空,指向队列头元素 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;

/* 循环队列的基本操作(9个) */
void InitQueue(SqQueue *Q)
{ /* 构造一个空队列Q */
Q->base=malloc(MAX_QSIZE*sizeof(QElemType));
if(!Q->base) /* 存储分配失败 */
exit(OVERFLOW);
Q->front=Q->rear=0;
}

void DestroyQueue(SqQueue *Q)
{ /* 销毁队列Q,Q不再存在 */
if(Q->base)
free(Q->base);
Q->base=NULL;
Q->front=Q->rear=0;
}

void ClearQueue(SqQueue *Q)
{ /* 将Q清为空队列 */
Q->front=Q->rear=0;
}

Status QueueEmpty(SqQueue Q)
{ /* 若队列Q为空队列,则返回TRUE;否则返回FALSE */
if(Q.front==Q.rear) /* 队列空的标志 */
return TRUE;
else
return FALSE;
}

int QueueLength(SqQueue Q)
{ /* 返回Q的元素个数,即队列的长度 */
return(Q.rear-Q.front+MAX_QSIZE)%MAX_QSIZE;
}

Status GetHead(SqQueue Q,QElemType *e)
{ /* 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR */
if(Q.front==Q.rear) /* 队列空 */
return ERROR;
*e=Q.base[Q.front];
return OK;
}

Status EnQueue(SqQueue *Q,QElemType e)
{ /* 插入元素e为Q的新的队尾元素 */
if((Q->rear+1)%MAX_QSIZE==Q->front) /* 队列满 */
return ERROR;
Q->base[Q->rear]=e;
Q->rear=(Q->rear+1)%MAX_QSIZE;
return OK;
}

Status DeQueue(SqQueue *Q,QElemType *e)
{ /* 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR */
if(Q->front==Q->rear) /* 队列空 */
return ERROR;
*e=Q->base[Q->front];
Q->front=(Q->front+1)%MAX_QSIZE;
return OK;
}

void QueueTraverse(SqQueue Q,void(*vi)(QElemType))
{ /* 从队头到队尾依次对队列Q中每个元素调用函数vi() */
int i;
i=Q.front;
while(i!=Q.rear)
{
vi(Q.base[i]);
i=(i+1)%MAX_QSIZE;
}
printf("\n");
}

4.链式队列

在队列的形成过程中,可以利用线性链表的原理,来生成一个队列。基于链表的队列,要动态动态创建和删除节点,效率较低,但是可以动态增长。一个链队列显然需要两个分别指示队头和队尾的指针才能唯一确定,,为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点,如图4-1-1所示。

空队列时,front和rear都指向头结点,如图4-1-2所示。

入队操作就是在链表尾部插入结点,如图4-1-3所示。

出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点如图4-1-4所示。

若链表除头结点只剩一个元素时,则需将rear指向头结点,如图4-1-5所示。

链式队列使用链表作为基本数据结果,所以不存在伪溢出的问题,队列长度也没有限制。但插入和读取的时间代价较高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/* 单链队列——队列的链式存储结构 */
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;

typedef struct
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;

/* 链队列的基本操作(9个) */
void InitQueue(LinkQueue *Q)
{ /* 构造一个空队列Q */
Q->front=Q->rear=malloc(sizeof(QNode));
if(!Q->front)
exit(OVERFLOW);
Q->front->next=NULL;
}

void DestroyQueue(LinkQueue *Q)
{ /* 销毁队列Q(无论空否均可) */
while(Q->front)
{
Q->rear=Q->front->next;
free(Q->front);
Q->front=Q->rear;
}
}

void ClearQueue(LinkQueue *Q)
{ /* 将Q清为空队列 */
QueuePtr p,q;
Q->rear=Q->front;
p=Q->front->next;
Q->front->next=NULL;
while(p)
{
q=p;
p=p->next;
free(q);
}
}

Status QueueEmpty(LinkQueue Q)
{ /* 若Q为空队列,则返回TRUE,否则返回FALSE */
if(Q.front->next==NULL)
return TRUE;
else
return FALSE;
}

int QueueLength(LinkQueue Q)
{ /* 求队列的长度 */
int i=0;
QueuePtr p;
p=Q.front;
while(Q.rear!=p)
{
i++;
p=p->next;
}
return i;
}

Status GetHead_Q(LinkQueue Q,QElemType *e)
{ /* 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR */
QueuePtr p;
if(Q.front==Q.rear)
return ERROR;
p=Q.front->next;
*e=p->data;
return OK;
}

void EnQueue(LinkQueue *Q,QElemType e)
{ /* 插入元素e为Q的新的队尾元素 */
QueuePtr p= (QueuePtr)malloc(sizeof(QNode));
if(!p) /* 存储分配失败 */
exit(OVERFLOW);
p->data=e;
p->next=NULL;
Q->rear->next=p;
Q->rear=p;
}

Status DeQueue(LinkQueue *Q,QElemType *e)
{ /* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next; /* 指向头结点 */
*e=p->data;
Q->front=p->next; /* 摘下头节点 */
if(Q->rear==p)
Q->rear=Q->front;
free(p);
return OK;
}

void QueueTraverse(LinkQueue Q,void(*vi)(QElemType))
{ /* 从队头到队尾依次对队列Q中每个元素调用函数vi() */
QueuePtr p;
p=Q.front->next;
while(p)
{
vi(p->data);
p=p->next;
}
printf("\n");
}

对于循环队列与链队列的比较,可以从两方面来考虑:

  1. 从时间上,其实它们的基本操作都是常数时间,即都为0(1)的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。

  2. 对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。

  3. 总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。

用数组实现队列时,如果不移动,随着数据的不断读写,会出现假满队列的情况。即尾数组已满但头数组还是空的。循环队列也是一种数组,只是它在逻辑上把数组的头和尾相连,形成循环队列,当数组尾满的时候,要判断数组头是否为空,不为空继续存放数据,可以有效的利用资源。但是用循环队列有个小麻烦,不好判断数列是为空还是为满;
链队列就不存在上面的问题。“循环队列”最大优点就是节省空间和少分配空间,而链队列多了一点点地址存储开销。