在# 06--队列的顺序实现已经介绍了队列的特征及顺序存储的实现,本文章将从设计队列的链式存储的时现。
一、队列的链式实现设计
队列的特征:
- 1.线性结构的特征队列都有,关于线性结构的特征可以看文章# 01--线性表的顺序存储;
- 2.队列的特殊限定性特征是:
先进先出;- 3.队列由
一个方向入队,一个方向出队。我们称入队的方向为队尾,出队的方向队头。
结合队列和特征和链式存储的特性,我们来设计一下队列的链式实现。
二、准备工作
设计一些回调状态和类型重定义
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */
三、节点的设计
这里我们选择单向链表来实现队列的链式实现。节点设计如下:
typedef struct QNode /* 结点结构 */
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
data表示
数据域,next是指针域,指向下一结点。
根据单向链表的特征和队列的特征(单向链表的特征可以看# 01--单向链表),我们设计如下结构体作为队列的链式实现:
typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;
队头为front,队尾为rear。
四、队列操作
队列的操作包括初始化队列、销毁队列、清空队列、队列判空、获取队列长度、入队、出队、获取队头元素和遍历队列。
1.初始化队列
Status InitQueue(LinkQueue *Q){
//1. 头/尾指针都指向新生成的结点
Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode));
//2.判断是否创建新结点成功与否
if (!Q->front) {
return ERROR;
}
//3.头结点的指针域置空
Q->front->next = NULL;
return OK;
}
- 1.设计一个
头结点作为队列为空时队头和队尾的统一指向,关于在链表中设计头结点带来的好处可以查看文章# 01--单向链表;- 2.队列
为空时,队头front和队尾rear指向同一位置,即头结点,只有头结点时,它的next指向NULL;- 3.
队头是出队的位置,队尾是入队的位置,它指向头结点,头结点的next指向下一个结点。如下图所示:
- 1.按照如上图设计,当
入队时,只需要创建新结点插入rear后面即可;- 2.
出队时拿到front的next即结点A,再拿到A的next即结点B,释放A结点,再将front的next指向B即可实现出队。
2.销毁队列
Status DestoryQueue(LinkQueue *Q){
//遍历整个队列,销毁队列的每个结点
while (Q->front) {
Q->rear = Q->front->next;
free(Q->front);
Q->front = Q->rear;
}
return OK;
}
- 1.销毁队列时,队头front和队尾rear已经失去存在的意义,所以可以把他们当做
辅助空间使用;- 2.销毁队列时,
头结点也没有存在的必要了,所以可以从头结点遍历到最后一个结点,并依次将它们进行释放;- 3.front指向头结点,把rear作为辅助空间指向待删除结点的下一个结点,方便下一次删除。
3.清空队列
Status ClearQueue(LinkQueue *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);
}
return OK;
}
- 1.清空队列时,
头结点需要保留,方便下一次入队;- 2.队列为空时,
队尾rear和队头front指向同一位置,即头结点;- 3.队列为空时,
队头的next指向为NULL;- 4.拿到队头后面的所有结点,依次遍历并释放。
4.队列判空
Status QueueEmpty(LinkQueue Q){
if (Q.front == Q.rear)
return TRUE;
else
return FALSE;
}
队列为空时,队头
front的队尾rear指向同一位置。
5.获取队列长度
int QueueLength(LinkQueue Q){
int i= 0;
QueuePtr p;
p = Q.front;
while (Q.rear != p) {
i++;
p = p->next;
}
return i;
}
- 1.从
队头front的next遍历到队尾,每次累加1,最后即可得到队列的长度;- 2.也可以在设计队列的结构体时加入队列的长度
length,在操作入队、出队和清空队列等操作时修改length的值。
6.入队
Status EnQueue(LinkQueue *Q,QElemType e){
//为入队元素分配结点空间,用指针s指向;
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
//判断是否分配成功
if (!s) {
return ERROR;
}
//将新结点s指定数据域.
s->data = e;
s->next = NULL;
//将新结点插入到队尾
Q->rear->next = s;
//修改队尾指针
Q->rear = s;
return OK;
}
- 1.队列的链式实现入队时
无需判满;- 2.创建一个结点,将新结点指入队尾,因为
队尾在单向链表的最后一个结点,所以新结点插入队列后成为了新的队尾,所以新结点的next指向NULL;- 3.新结点
入队后,记得将队尾rear指向新结点。
7.出队
Status DeQueue(LinkQueue *Q,QElemType *e){
QueuePtr p;
//判断队列是否为空;
if (Q->front == Q->rear) {
return ERROR;
}
//将要删除的队头结点暂时存储在p
p = Q->front->next;
//将要删除的队头结点的值赋值给e
*e = p->data;
//将原队列头结点的后继p->next 赋值给头结点后继
Q->front->next = p ->next;
//若队头就是队尾,则删除后将rear指向头结点.
if(Q->rear == p) Q->rear = Q->front;
free(p);
return OK;
}
- 1.出队时,需要判断队列是否
为空,为空时不能出队;- 2.队头
front指向头结点,头结点的next指向队列的第一个有效数据;- 3.出队前需要拿到
待删除结点和待删除结点的下一个结点,删除待删除结点后,将头结点的next指向删除结点的下一个结点;- 4.如果
删除的结点是队尾,而需要将队尾rear指向队头front。
8.获取队头元素
Status GetHead(LinkQueue Q,QElemType *e){
//队列非空
if (Q.front != Q.rear) {
//返回队头元素的值,队头指针不变
*e = Q.front->next->data;
return TRUE;
}
return FALSE;
}
- 1.获取
队头数据时,需要判空;- 2.因为队头指向的是
头结点,所以获取队头数据应该获取的是front->next的data。
9.遍历队列
Status QueueTraverse(LinkQueue Q){
QueuePtr p;
p = Q.front->next;
while (p) {
printf("%d ",p->data);
p = p->next;
}
printf("\n");
return OK;
}
队列的
遍历和单向链表的遍历一样,从队头,即头结点的next开始,直到next指向为NULL时结束。
五、总结
队列的链式存储的实现使用了单向链表的数据结构,并巧妙的设计了一个头结点,将队头front指向头结点,方便了出队;将队尾rear指向了单向链表的最后一个结点,方便了入队。