数据结构算法代码实现——栈和队列(二)

队列的定义

和栈相反,队列(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,顺序栈和顺序队列容易产生“溢出”现象,另外顺序队列还容易出现“假溢出”现象。因此,
以循环队列作为队列的顺序存储结构,并采用“牺牲”一个存储结点或设置一个标志位的方法,可以区分循环队列的“队空”和“队满”条件。
发布了17 篇原创文章 · 获赞 21 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/hou1620089770/article/details/46508479