队列的定义
和栈相反,队列(queue)是一种先进先出(FIFO)的线性表。它只允许在表的一端经行插入,而在另一端删除删除。
我们把允许进行删除的一端称为队头,允许插入的一端称为队尾。
在插入新元素到队尾的操作简称进队或入队,从队列中删除队头元素的操作简称出队或离队,若队列中无数据元素则称为空队列。
队列的表示与实现
和线性表类似,队列也可以有两种存储表示。
1,队列的链式表示与实现。
(1)
用链表示的队列简称为链队列。和单链表一样,为了方便我们也可以为队列加一个“头结点”,由于队列的特点必须要在队头、队尾操作,
所以有必要为其添加队头和队尾的指针(分别称为头指针和尾指针),然后
令头指针指向头结点。
空的队列判断条件:头指针和尾指针均指向头结点。
(2)队列的链式存储结构
//-----单链队列---队列的链式存储结构
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
typedef struct {
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
(3)基本操作代码:
//--------基本操作-----
//1
Status InitQueue(LinkQueue *Q){
//构造一个空队列
(*Q).front =(QueuePtr)malloc(sizeof(QNode));//生成头结点
if(!(*Q).front) exit(OVERFLOW);
(*Q).front->next =NULL; //头结点的指针域指向空
(*Q).rear =(*Q).front;
return OK;
}
//2
Status DestroyQueue(LinkQueue *Q){
//销毁一个队列
while((*Q).front){ //从队头释放资源
(*Q).rear =(*Q).front->next;
free( (*Q).front);
(*Q).front =(*Q).rear;
}
return OK;
}
//3
Status ClearQueue(LinkQueue *Q){
QueuePtr p,q;
p =(*Q).front->next; //指向队列的第一个结点
while(p){ //从第一个结点释放资源
q =p->next;
free( p);
p =q;
}
(*Q).front->next=NULL;
(*Q).rear =(*Q).front;//为空的条件
return OK;
}
//4
Status QueueEmpty(LinkQueue Q){
if(Q.front == Q.rear)
return OK;
else
return ERROR;
}
//5
int QueueLength(LinkQueue Q){
int j=0;
QueuePtr p =Q.front;
while(p !=Q.rear){ //从队头直到和队尾相同时结束
j++;
p =p->next;
}
return j;
}
//6
Status GetHead(LinkQueue Q,QElemType *e){
//返回队头元素
QueuePtr p;
//判断不为空
if(Q.front == Q.rear)
return ERROR;
p =Q.front->next;
*e =p->data;
return OK;
}
//7
/*
算法思想:
1,开辟一个新结点p,为新结点的数据域和指针域分别赋值
2,将新结p链接到队尾后,即把队尾结点的指针域指向新的结点p
3,将尾指针指向p
*/
Status EnQueue(LinkQueue *Q,QElemType e){
//插入元素
QueuePtr p=(QueuePtr)malloc(sizeof(QNode)); //申请结点
if(!p)
exit(OVERFLOW);
p->data=e;
p->next=NULL;
//在队尾插入
(*Q).rear->next=p;
(*Q).rear=p;
return OK;
}
//8,
/*
算法思想:
1,判断队列不为空
2,使待删除p指向队头的第一个结点,在删除前还需要保存该结点的信息。
3,根据队列的操作逻辑定义,把队头结点的指针域指向p的下一个结点。
4,判断待删除p是否指向了队尾结点,如果指向了队尾,则把队尾指针指向队头指针
*/
Status DeQueue(LinkQueue *Q,QElemType *e){
QueuePtr p;
if((*Q).front ==(*Q).rear) return ERROR;
p =(*Q).front->next;//待删除结点p
*e =p->data;
(*Q).front->next =p->next;
if((*Q).rear ==p){//特殊判断,当待删除的结点是最后一个时,需要特殊处理。
(*Q).rear =(*Q).front;
}
free(p);
return OK;
}
Status QueueTraverse(LinkQueue Q,void(*vi)(QElemType))
{
QueuePtr p;
p=Q.front->next;
while(p)
{
vi(p->data);
p=p->next;
}
printf("\n");
return OK;
}
测试代码:
#include"ch2.h"
typedef int QElemType;
#include"Queue.c"
void visit(QElemType q)
{
printf("%d ",q);
}
void main()
{
int i;
QElemType e;
LinkQueue q;
i=InitQueue(&q);
if(i)
printf("成功构造了一个空队列\n");
printf("队列是否为空?%d(1:空 0:否) ",QueueEmpty(q));
printf("队列的长度为%d\n",QueueLength(q));
//插入数据结点
EnQueue(&q,10);
EnQueue(&q,20);
EnQueue(&q,30);
printf("插入3个元素(10,20,30)后,队列的长度为%d\n",QueueLength(q));
printf("队列是否为空?%d(1:空 0:否)\n ",QueueEmpty(q));
printf("队列的元素依次为:");
QueueTraverse(q,visit);
//取得队元素
i=GetHead(q,&e);
if(i==OK)
printf("队头元素是:%d\n",e);
//删除数据结点
DeQueue(&q,&e);
printf("删除队头第一个结点数据%d\n",e);
i=GetHead(q,&e);
if(i==OK)
printf("新的队头第一个结点数据是:%d\n",e);
//清空与销毁
ClearQueue(&q);
printf("清空队列后,q.front=%d q.rear=%d q.front->next=%d\n",q.front,q.rear,q.front->next);
DestroyQueue(&q);
printf("销毁队列后,q.front=%d q.rear=%d\n",q.front, q.rear);
}
测试结果图:
2.队列的顺序表示与实现
(1)先讨论一下两种非循环队列的顺序存储结构
第一种:
#define QUEUE_INIT_SIZE 10 //初始分配量
#define QUEUE_INCREMENT 5 //分配增量
typedef struct {
QElemType *base; //初始化时动态分配的存储空间
int rear; //尾指针,队列不为空时,指向数组元素下一个位置(存放的是数组的下标)
int queuesize; //当前分配的空间大小
}SQueue;
通过上述形式:可以看出只设置了尾指针,也就是说在插入元素时只需要移动尾指针就行了。然而没有设置头指针,
所以只能让数组的【0】单元来充当队头元素(数组基地址充当头指针),当我们在队头([0]单元)执行删除操作时,
为了保证队头元素总在【0】单元,所以在删除时都需要移动数据元素,这也是它最大的缺点。
第二种:
#define QUEUE_INIT_SIZE 10 //初始分配量
#define QUEUE_INCREMENT 5 //分配增量
typedef struct {
QElemType *base; //初始化时动态分配的存储空间
int front; //头指针,若队列不为空只想队头元素。
int rear; //尾指针,队列不为空时,指向数组元素下一个位置(存放的是数组的下标)
int queuesize; //当前分配的空间大小
}SQueue;
首先注意的一点,它与之前顺序栈的不同,队列的头尾指针均是保存的数组元素的下标。还有它与第一种形式的不同:只增加了一个头指针。
从增加的这个的头指针可以看出,在队头执行删除操作时,肯定不需要大量移动元素了,只需修改头指针的内容+1就行了,
但是,这种形式的缺点也就显而易见了,删除的元素的空间不能在重复利用,当队列满时但还有空间(假溢出),造成空间浪费大。
(2)使用循环队列
为了克服上述的缺点,我们可以采用循环队列。为了重新利用队列前面删除的空间,避免“假溢出”的发生,
可以将整个数组空间变成一个首尾相连的圆环,也可以称作循环数组。
#define MAXQSIZE 100 // 最大队列长度
typedef struct
{
QElemType *base; // 初始化的动态分配存储空间
int front; // 头指针,若队列不空,指向队列头元素
int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
又因为计算机的内存并不是环状的,所以我们必须人为的定义法则,使其适应计算机的存储。可以使用下面代码来判断
(1)
if(rear+1 ==MAXQSIZE)
rear=0;
else
rear++;
其实我们使用取余的“模运算”使程序更简洁。front=(front+1)%MAXQSIZE。通过这种方式,指针front和rear会沿着圆盘不停的旋转,从而形成了循环的队列。
(3)循环队列的顺序结构基本操作:
//--------循环队列的基本操作----------
//1,初始化一个队列
Status InitQueue(SqQueue *Q)
{
(*Q).base =(QElemType*)malloc(MAXQSIZE*sizeof(QElemType));
if(!(*Q).base) exit(OVERFLOW);
(*Q).front =(*Q).rear =0;
return OK;
}
//2销毁
Status DestroyQueue(SqQueue *Q)
{
if((*Q).base)
free((*Q).base);
(*Q).front =(*Q).rear=0;
(*Q).base =NULL;
return OK;
}
//3清空
Status ClearQueue(SqQueue *Q)
{
(*Q).front =(*Q).rear =0;
return OK;
}
//4是否为空
Status QueueEmpty(SqQueue Q){
if( Q.rear ==Q.front)
return TRUE;
else
return FALSE;
}
//5队列的长度
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
//6获取队头元素
Status GetHead(SqQueue Q,QElemType *e)
{
//判断不为空,这里舍弃一个空间
if( Q.rear ==Q.front)
return ERROR;
*e =Q.base[Q.front];
return OK;
}
//7插入操作
/*
算法思想:
1,判断队列不为满,(Q.rear+1)%MAXQSIZE ==Q.front
2,在队尾插入新元素
3,队尾指针加1
*/
Status EnQueue(SqQueue *Q,QElemType e)
{
if( ((*Q).rear+1)%MAXQSIZE ==(*Q).front)
return ERROR;
(*Q).base[(*Q).rear] =e;
(*Q).rear =((*Q).rear+1)%MAXQSIZE;
return OK;
}
//8删除操作
/*
算法思想:
1,先判断循环队列不为空。
2,删除队头指针的数据元素,在此之前保存待删除元素的信息。
3,队头指针加1。
*/
Status DeQueue(SqQueue *Q,QElemType *e)
{
if((*Q).rear ==(*Q).front)
return ERROR;
*e =(*Q).base[(*Q).front];
(*Q).front =((*Q).front+1)%MAXQSIZE;
return OK;
}
//9输出
Status QueueTraverse(SqQueue Q,void(*visit)(QElemType))
{
int i;
i =Q.front;
while(i!=Q.rear){
(*visit)( Q.base[i] );
i=(i+1)%MAXQSIZE;
}
printf("\n");
return OK;
}
测试代码:
#include"ch2.h"
typedef int QElemType;
#include"Loop_queue.c"
void visit(QElemType q)
{
printf("%d ",q);
}
void main()
{
Status j;
int i=0;
QElemType q;
SqQueue Q;
//初始化
InitQueue(&Q);
printf("初始化队列后,队列空否?%u(1:空 0:否)\n",QueueEmpty(Q));
//插入,长度
printf("请输入整型队列元素(不超过%d个),-1为提前结束符: ",MAXQSIZE-1);
do
{
scanf("%d",&q);
if(q==-1)
break;
i++;
EnQueue(&Q,q);
}while(i
结果图:
(4)循环队列的注意问题:
从上述代码中可见,在C语言中不能用动态分配的一维数组来实现循环队列。如果用户的应用程序中没有循环队列,
则必须为它设定一个最大队列长度,若用户无法估计最大长度,则易使用链队列
当循环队列执行出队操作时:队头指针顺时针追赶队尾指针,当front等于rear时,循环队列为空队。
当循环队列执行入队操作时:队尾指针顺时针追赶队头指针,当front等于rear时,循环队列为满。
由此可见,无法辨别当front等于rear时,是“空”还是“满”。
解决方法:(1)可以采用牺牲一个存储单元的方法,即当队尾指针顺时针的下一个位置为队头时
【(rear+1)%MAXQSIZE==front】,这样rear无法追上front,当追上时也就是“空队”了。
(2)设置一个标志位以区别队列是满还是空。比如说:设置flag,开始时flag=0,入队flag=1
出队flag=0。然后再加上判断队头队尾指针是否重合,当重合时,且flag=0,则为空、且flag=1,则为满。
栈和队列总结
1,栈是限定只能在表的一端经行插入与删除的线性表,其特点是先进后出。
2,队列是限定在表的一端(队尾)进行插入,而在表的另一端(队头)进行删除的线性表,其特点是先进先出。
3,栈和队列都有两种不同的存储方式,即顺序存储结构和链式存储结构。
4,顺序栈和顺序队列容易产生“溢出”现象,另外顺序队列还容易出现“假溢出”现象。因此,
以循环队列作为队列的顺序存储结构,并采用“牺牲”一个存储结点或设置一个标志位的方法,可以区分循环队列的“队空”和“队满”条件。