定义和特点
和栈相反,队列(queue)是一种先进先出的线性表。它只允许在表的一端进行插入,而在另一端删除元素。允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。
假设队列为q=(a1,a2,…,an),那么,a1就是队头元素,an就是队尾元素。队列中的元素是按照a1,a2,…,an的顺序进入的,退出队列也只能按照这个次序依次退出。
队列的抽象数据类型定义:
ADT Queue{
数据对象:D={ai | ai∈ElemSet,i=1,2,…,n,n≥0}
数据关系:R={< a(i-1) , ai > | a(i-1), ai ∈ D,i=2,…,n}
基本操作:
InitQueue(&Q) 构造一个空队列Q
DestroyQueue(&Q) 销毁队列Q
ClearQueue(&Q) 清空队列Q
QueueEmpty(Q) 若Q为空队列,返回true,否则返回false
QueueLength(Q) 返回Q的元素个数,即队列的长度
GetHead(Q) 返回Q的队头元素
EnQueue(&Q,e) 插入元素e为Q的新的队尾元素
DeQueue(&Q,&e) 删Queue除Q的队头元素,并用e返回其值
QueueTraverse(Q)从队头到队尾,依次对Q的每个元素访问
}ADT Queue
循环队列——队列的顺序表示和实现
队列的顺序存储结构:
#define MAXQSIZE 100 //队列可能达到的最大长度
typedef struct
{
QElemType *base; //存储空间的基地址
int front; //头指针
int rear; //尾指针
}SqQueue;
为了在c语言中描述方便起见,在此约定:初始化创建空队列时,令front=rear=0,每当插入新的队列尾元素时,尾指针rear增1;每当删除队列头元素时,头指针front增1。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置,如图:
(d)图状态时不可再继续插入新的队尾元素,否则会出现溢出现象,即因数组越界而导致程序的非法操作错误。事实上此时的队列实际可用空间并未占满,所以称为这种现象为“假溢出”现象。
怎样解决假溢出问题?一个巧妙的办法是将顺序队列变为一个环状的空间,称之为循环队列。
头、尾指针以及队列元素之间的关系不变,只是在循环队列中,头、尾指针“依次增1”的操作可用“模”运算来实现。通过取模,头指针和尾指针就可以在顺序表空间内以头尾衔接的方式“循环”移动。
在图3.14(a)中,队头元素是J5,在元素J6入队之前,在Q.rear的值为5,当元素J6入队之后,通过“模”运算,Q.rear=(Q.rear+1)%6,得到Q.rear的值为0.而不会出现“假溢出”现象。
在图3.14(b)中,J7,J8,J9,J10相继入队,则队列空间均被栈满,此时头指针、尾指针相同。
在图3.14(c)中,若J5和J6相继从图3.14(a)所示的队列出队,使队列此时呈“空”的状态,头、尾指针也是相同的。
那要怎样区别队满还是队空呢?
处理方法:
少用一个元素空间,即队列空间大小为m时,有m-1个元素就认为是队满。
这样判断队空的条件不变(即头尾指针的值相同),则为队空;而当尾指针在循环意义上加1后是等于头指针,则认为是队满。
队空的条件:Q.front == Q.rear
队满的条件:(Q.rear+1)%MAXQSIZE==Q.front
如图3.14(d)所示,当J7、J8、J9进入图3.14(a)所示的队列后,(Q.rear+1)%MAXQSIZE的值等于Q.front,此时认为队满。
//base[0]接在base[MAXQSIZE-1]之后,若rest+1==M,则令rear=0;
插入元素:Q.base[Q.rear]=x;
Q.rear=(Q.rear+1)%MAXQSIZE;
删除元素:x=Q.base[s.front];
Q.front=(Q.front+1)%MAXQSIZE;
①初始化
循环队列的初始化操作就是动态分配一个预定义大小为MAXQSIZE的数组空间。
Status InitQueue(SqQueue &Q)
{//构造一个空队列
Q.base==new QElemType[MAXQSIZE]; //为队列分配一个最大容量为MAXQSIZE的数组空间
if(!Q.base) exit(OVERLOW); //存储分配失败
Q.front=Q.rear=0; //头指针和尾指针置为零,队列为空
return OK;
}
②求队列长度
对于非循环队列,尾指针和头指针的差值便是队列长度,而对于循环队列,差值可能为负数,所以需要将差值加上MAXQSIZE,然后与MAXQSIZE求余
int QueueLength(SqQueue Q)
{//返回Q的元素个数,即队列的长度
return(Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
③入队
入队操作是指在队尾插入一个新的元素
Status EnQueue(SqQueue &Q,QElemType e)
{//插入e为Q的新的队尾元素
if((Q.rear+1)%MAXQSIZE==Q.front) //队满
return ERROR;
Q.base[Q.rear]=e; //新元素插入队尾
Q.rear=(Q.rear+1)%MAXQSIZE; //队尾指针加1
return OK;
}
④出队
出队操作是将队头元素删除
Status DeQueue(SqQueue &Q,QElemType &e)
{//删除Q的队头元素,用e返回其值
if(Q.front==Q.rear) return ERROR; //队空
e=Q.base[Q.front]; //保存队头元素
Q.front=(Q.front+1)%MAXQSIZE; //队头指针加
return OK;
}
⑤取队头元素
当队列非空时,此操作返回当前队头元素的值,队头指针保持不变
SElemType GetHead(SqQueue Q)
{//返回Q的队头元素,不修改队头指针
if(Q.front!=Q.rear) //队列非空
return Q.base[Q.front]; //返回队头元素的值,队头指针不变
}
链队——队列的链式表示和实现
为了操作方便起见,给链队添加一个头结点,并令头指针始终指向头节点。
队列的链式存储结构:
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct
{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
①初始化
队列的初始化操作就是构造一个只有头节点的空队
Status InitQueue(LinkQueue &Q)
{//构造一个空队列
Q.front=Q.rear=new QNode; //生成新结点作为头结点,队头和队尾指针指向此结点
Q.front->next=NULL; //头结点的指针域置空
return OK;
}
②入队
和循环队列的入队操作不同的是,链队在入队前不需要判断队是否满,需要为入队元素动态分配一个结点空间
Status EnQueue(LinkQueue &Q,QElemType e)
{//插入元素e为Q的新的队尾元素
p=new QNode; //为入队元素分配结点空间,用指针p指向
p->data=e; //将新结点的数据域置为e
p->next=NULL; Q.rear->next=p; //将新结点插入到队尾
Q.rear=p; //修改队尾指针
return OK;
}
③出队
和循环队列一样,链队在出队前也需要判断队列是否为空,不同的是,链队在出队后需要放出队头元素的所占空间
Status DEQueue(LinkQueue &Q,QElemType &e)
{//删除Q的队头元素,用e返回其值
if(Q.front==Q.rear) return ERROR; //队列空
p=Q.front->next; //p指向队头元素
e=p->data; //e保存队头元素的值
Q.front->next=p->next; //修改头结点的指针域
if(Q.rear==p) Q.rear=Q.front; //最后一个元素被删,队尾指针指向头结点
delete p; //释放原队头元素空间
return OK;
}
④取队头元素
与循环队列一样,当队列非空时,此操作返回当前队头元素的值,队头指针保持不变
SElemType GetHead(LinkQueue Q)
{
if(Q.front!=Q.rear) //队列非空
return Q.front->next->data; //返回队头元素的值,队头指针不变
}
借鉴:《数据结构》 严蔚敏