队列(Queue)是只允许在一端进行插入工作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
队列的抽象数据类型:
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestoryQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(Q):若队列为空,则返回true,否则返回false。
GetHead(Q,*e):若队列Q存在且非空,用e来返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中成为队尾元素。
DeQueue(*Q,*e):删除队列Q的队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。
假设队列有n个元素,则顺序存储的队列需建立一个大于n的数组,并将n个元素存储在数组的前n个单元中,数组下标为0的一端为队头。所谓的插入队列操作,其实是在队尾追加一个元素,不需要移动任何元素,时间复杂度为 O(1) 。和栈不同,队列的出列操作在队头进行,即下标为0的位置,即队列中的所有元素都要向前移动,保证队列的队头在下标为0的位置且不为空,此时的时间复杂度为 O(n) 。
仔细想想,为何出队列是一定要全部移动呢?若不限制队列的元素不需要存储在数组的前n个单元,即队头不需再下标为0的位置,这样能提高队列出列的性能。
因此,引入两个指针,front指针指向队头元素,rear指针指向队尾指针元素的下移位置,这样当front等于rear时,该队列为空队列。
假设这个队列的总数不超过5个,但是因为数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,队列在数组前面的位置还是空闲的,这种情况就是”假溢出”现象。
要解决”假溢出”问题的方法就是后面满了,从头再开始,即头尾相接的循环,队列的这种头尾相接的顺序存储结构被称为循环队列。以上述为例,数组末尾元素占用后,将尾指针改为指向下标为0的位置,继续插入。
又有问题来了,空队列时,front==rear,而队列满时,还是front==rear,那如何判断队列是空还是满呢?
第一种方法,设置标志变量,当front==rear且flag==0时队列为空,当front==rear且flag==1时队列为满;
第二种方法,当front==rear时,队列为空,当队列满是,修改其条件,保留一个元素空间(当队列满是,数组中还有一个空闲单位)。假设队列的最大尺寸为QueueSize,则队列满时的条件为(rear+1)%QueueSize==front。(因为rear可能大于也可能小于font,但它们之间总是相差一个元素空间)
循环队列的顺序存储结构代码如下,部分方法也如下:
typedef int QElemType;//QElemType根据实际情况而定,这里设置为int
/*循环队列的顺序存储结构*/
typedef struct{
QElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不为空,指向队尾元素的下一位置
}SqQueue;
/*初始化一个空队列*/
Status InitQueue(SqQueue *Q){
Q->front=0;
Q->rear=0;
return OK;
}
/*返回Q的元素个数,即队列的长度*/
int QueueLength(SqQueue Q){
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
/*若队列未满,则插入元素e为Q的新的队尾元素*/
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;
return OK;
}
/*若队列不为空,则删除队列Q的队头元素,并用e返回其值*/
Status DeQueue(SqQueue *Q,QElemType *e){
if(Q->rear==Q->front) return ERROR;//队列为空
*e=Q->data[Q->front]; //将队头元素赋值给e
//队头指针后移一位,若到数组最后转向数组头部
Q->front=(Q->front+1)%MAXSIZE;
return OK;
}
若单是顺序存储,不是循环队列,算法的时间性能不高,但是循环队列又面临数组可能溢出问题,所以需要好好研究下队列的链式存储结构。
队列的链式存储结构,其实是线性表的单链表,只不过是只能尾进头出而已,将其简称为链队列。将队头指针指向链队列的头结点,队尾指针指向终端结点。当队列为空时,front和rear均指向头结点。
链队列的结构代码如下:
typedef int QElemType;
typedef struct QNode{ //结点结构
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct{ //链队列结构
QueuePtr front,rear; /*队头、队尾指针*/
}LinkQueue;
下面详细介绍链队列的入队、出队操作及具体代码:
/*插入元素e为Q新的队尾元素*/
Status EnQueue(LinkQueue *Q,QElemType e){
//首先生成一个新的元素
QueuePtr s=(QueuePtr)malloc (sizeof(QNode));
if(! s) exit(OVERFLOW); //存储分配失败
s->data=e; //设置元素的值
s->next=NULL; //末尾元素,下一结点为空
Q->rear->next=s; //插入新元素,将原队尾元素的指针域指向新结点
Q->rear=s; //将队尾指针后移,指向新元素
return OK;
}
/*若队列不为空时,删除队列Q的队头元素,并用e来返回其值,并返回OK,否则返回ERROR*/
Status DeQueue(LinkQueue *Q,QElemType *e){
QueuePtr p;
if(Q->front==Q->rear) return ERROR; //队列为空
p=Q->front->next; //要删除的结点
*e=p->data; //取得要删除结点的值
Q->front->next=p->next; //删除队头元素结点
//若队头是队尾,则删除后将尾指针指向头结点,注意只有一个元素的情况
if(Q->rear==p) Q->rear=Q->front;
free(p);
return OK;
}
循环队列与链队列的对比:
时间上,基本操作均为常数时间 O(1) ,不过循环队列事先申请存储空间,且使用期间不释放,而链队列,每次申请和释放结点均会产生时间开销,若入出队频繁,二者还是存在差异的。就空间上来说,循环队列有一固定长度,所以存在空间浪费的情况,虽然链队列不存在这个情况,但会产生空间开销,在可接受范围内,所以空间上链队列要更灵活些。总而言之,在能确定队列长度最大值的情况下,建议使用循环队列,如果无法预估队列长度时,使用链队列。