数据结构与算法(七)——队列

目录

一、前情提要

二、队列ADT的介绍与分析

(一)什么是队列?

(二)队列的抽象数据类型

  (三)队列的存储结构

      队列的顺序存储结构

      队列的链式存储结构

 三、代码实现

(一)队列的顺序存储结构——循环队列

(二)队列的链式存储结构——链队列


一、前情提要

        上一篇讲到了一种特殊的线性表,叫做栈。像栈一样,队列(queue)也是一种特殊的线性表。不同的是:入栈和出栈的顺序是先进后出,而入队和出队的顺序是先进先出。本篇会介绍这种数据结构并给出实现代码。

二、队列ADT的介绍与分析

(一)什么是队列?

       定义:队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

       从这个定义我们就能看出来和栈的区别,栈的定义是:限定仅在表尾进行插入和删除操作的线性表。

       所以我们就可以总结出队列的性质:

       1.队列是一种先进先出(First In First Out)的线性表,简称FIFO。

       2.只允许插入的一端称为队尾,允许删除的一端称为队头。

       

        我们画张图理解一下:

 (二)队列的抽象数据类型

        同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。

ADT  队列(queue)
Data
     同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
	 
Operation
     InitQueue(*Q):初始化操作,建立一个空队列。
	 DestroyQueue(*Q):若队列存在,则销毁它。
	 ClearQueue(*Q):将队列清空。
	 QueueEmpty(Q):若队列为空,返回true,否则返回false。
	 GetHead(Q):若队列存在且非空,返回Q的队头元素。
	 Push(*Q,*e):若队列存在,插入新元素e到队列中并成为队尾元素。
	 Pop(*Q,*e):删除队列中队头元素,并返回其值。
	 QueueLength(S):返回队列的元素个数。

endADT

(三)队列的存储结构

      线性表有顺序存储和链式存储,栈是线性表,具有这两种存储方式。同样的,队列作为一种特殊的线性表,也同样存在这两种存储方式。我们先来看队列的顺序存储结构。

      先来说说顺序存储:

      队列的顺序存储结构

      队列顺序存储的不足:

      我们假设一个队列有n个元素,则顺序存储的队列需要建立一个大于n的数组,并把所有的元素存储在前n个单元内,数组下标为0的一端就是队头。

      首先入队操作,就是在队尾追加一个元素,此时不需要移动任何元素,因此只需要O(1)的时间复杂度就可以完成。

      其次出队操作,就是在队头弹出一个元素,即在下标为0的位置操作。

      我画张图看一下:

      我们会发现第一个位置元素出队之后,就空着了,队头还在下标为0的位置处。没办法,我们只能将后面的元素集体向前挪一位,给他填满。

这样出队的时候就要挪动所有元素,所以出队操作的时间复杂度是O(n)。

这样的话出队操作太慢了!!! 

我们可以考虑用Front和Rear指针来进行优化。

Front指针指向队头元素,Rear指针指向队尾元素

所以就优化之后的图会变成这样:

 直接移动Front指针即可。

Q:为啥Rear指针要在队尾元素的后一位?

A:为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以Rear指针指向队尾元素的下一位。这样一来,当Front和Rear指向一个地方时,此队列并不是还剩一个元素,而是空队列!!

你以为优化完了吗????

我们知道Rear指针是指向队尾元素的后一个位置,如果队尾元素已经是最后一个单元格了呢?Rear难道移动到数组之外吗?显然还存在问题。

如图:

        第一个问题:Rear指针会移动到数组之外。

        还存在一个问题:如果元素继续增加,新元素a7该放在哪?目前如果接着入队的话,因为数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上我们队列在下标为0的地方还是空闲的。我们把这种现象叫做假溢出。 

        在现实生活中,你上了一辆公交车,发现前排有一个空座位,而后排全部坐满了,你会怎么做?下车不做了,等下一辆?没有这么笨的人,前面有座位,当然是可以坐的,除非坐满了,才会考虑下一辆。

     

       为了解决这两个问题,迎来了队列顺序存储结构的最终改良版本———循环队列。

       循环队列的定义:解决假溢出的办法就是后面满了,咱就从前面开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。

       刚才的例子继续,上图中的Rear指针移动到队尾元素的下一个位置,因为是头尾相接的,最终它可以指向下标为0的位置。

      我们最后再添加一个元素,让它变满。 

 此时问题又来了:我们刚刚说,空队列时,Front等于Rear,现在当队列满的时候,也是Front等于Rear,那么如何判断此时的队列究竟是空队列还是满的呢?

方法一:直接设置一个flag标志变量,当Front==Rear,且flag=0时为队列空,当Front==Rear,且flag=1时队列满。

方法二(一般用这个):当队列空时,条件就是Front==Rear,当队列满的时候,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还存在一个空闲单元。例如下图中的情况(至少还存在一个空位,无论这个空位在哪),我们就认为队列已经满了,也就是说我们不允许上图情况(所有单元格都被占)的情况发生。

   我们重点来讨论第二种方法,由于Rear可能比Front大,也可能比Front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差一整圈。所以若队列的最大尺寸为QueueSize,那么队列满的条件就是(Rear+1)%QueueSize==Front(%取模的目的就是为了整合Rear和Front大小的一个问题)。

    比如上面这个例子,QueueSize=7,Rear=0,Front=1,根据公式 

 (Rear+1)%QueueSize==Front     (0+1)%7=1  符合条件,所以此时队列是满的。

    还有一个公式,通用的计算队列长度公式:(Rear - Front + QueueSize)%QueueSize

    最终代码会在最后给出。

   接下来就是队列的链式存储结构:

   队列的链式存储结构

   队列的链式存储结构,其实就是线性表的单链表,只不过他只能尾进头出而已,我们把它简称为链队列。为了方便操作,我们将队头指针指向头结点,而队尾指针指向终端结点。

   空队列的时候,Front和Rear都指向头结点。

 

    这个入队和出队操作就很简单了,就是尾插和普通的删除操作。

    直接看后面代码吧。

三、代码实现

(一)队列的顺序存储结构——循环队列

  

#include <stdio.h>
#include <stdlib.h>
#define MaxSize 10

typedef struct
{
	int Data[MaxSize];
	int Front;
	int Rear;
}CircularQueue;
//队列初始化 
void InitQueue(CircularQueue* Queue)
{
	Queue->Rear=0; //首尾指针都指向同一位置 
	Queue->Front=0;
}
//入队操作 
void Push(CircularQueue* Queue,int Data)
{
	if((Queue->Rear+1)%MaxSize==Queue->Front)
	{
		printf("已满\n");//队列已满 
	}
	else
	{
		Queue->Data[Queue->Rear]=Data;      //输入当前结点数据 
		Queue->Rear=(Queue->Rear+1)%MaxSize;//尾指针向后移一位,取模是因为怕尾指针越界 
	} 
} 
//出队操作 
void Pop(CircularQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("空队列无法删除\n");
	}
	else
	{
		Queue->Front=(Queue->Front+1)%MaxSize;//头指针向后移一位。取模是因为怕指针越界 
	}
}
//输出队列长度 
void QueueLength(CircularQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("0\n");
	}
	else
	{
		printf("%d\n",(Queue->Rear-Queue->Front+MaxSize)%MaxSize);//由于尾指针可能再头指针前面,所以有可能为负值,取模转为正. 
	}
}
//查询队头的数据 
void GetHead(CircularQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("空队列\n"); 
	}
	else
	{
	   printf("%d\n",Queue->Data[Queue->Front]);
	}
}
//判断是否为空队列
void QueueEmpty(CircularQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("为空队列\n");
		return ;
	}
	else 
	{
		printf("该队列不为空\n");
	}
} 
//清空队列
void ClearQueue(CircularQueue* Queue)
{
	Queue->Rear=Queue->Front;//头指针和尾指针都放在一起,代表为空。 
} 
int main()
{
	CircularQueue Queue;
	InitQueue(&Queue);
	Push(&Queue,1);
	Push(&Queue,2);
	Push(&Queue,3);
	Push(&Queue,4);
	GetHead(&Queue);
	Pop(&Queue);
	GetHead(&Queue);
	QueueLength(&Queue);
	QueueEmpty(&Queue);
	ClearQueue(&Queue);
	QueueEmpty(&Queue);
	return 0;
}

 

 

(二)队列的链式存储结构——链队列
#include <stdio.h>
#include <stdlib.h>

typedef struct QueueNode
{
	int Data;
	QueueNode* Next;
}QNode,*QLinkList;  //队列的结点,指向结点的指针 

typedef struct LinkQueue
{
	QLinkList Front,Rear; 
}LinkQueue;        //定义头指针和尾指针 

//初始化队列 
void InitQueue(LinkQueue* Queue)
{
	Queue->Front=Queue->Rear=(QNode*)malloc(sizeof(QNode));//头指针和尾指针指向头结点 
	if(Queue->Front==NULL)
	{
		printf("内存分配失败");	
	}
	else
	{
		Queue->Front->Next=NULL;//初始化头结点的Next,指向NULL 
	}
}
//入队操作 
void Push(LinkQueue* Queue,int Data)
{
	QNode* New_Node=(QNode*)malloc(sizeof(QNode));
	if(New_Node==NULL)
	{
		printf("内存分配失败");	
	}
	else
	{
		New_Node->Data=Data;//输入数据 
		New_Node->Next=NULL;//新结点的Next指向NULL 
		Queue->Rear->Next=New_Node;//尾指针指向的结点的Next指向新结点,建立联系 
		Queue->Rear=New_Node;//尾指针指向新结点 
	}
} 

//出队操作
void Pop(LinkQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("为空队列,无法出队");
	}
	else
	{
		QNode* Ptr=Queue->Front->Next; //先建立临时指针记录要出队的结点 
		Queue->Front->Next=Ptr->Next;  //头指针指向要出队结点的下一个结点 
		if(Queue->Rear==Ptr) Queue->Rear=Queue->Front;//此处要特判一下,如果队中只有一个结点
		                                              //删除后要将头指针和尾指针一起指向头结点。 
		free(Ptr);  //最后释放出队元素的内存 
	}
} 
//输出队头元素 
void GetHead(LinkQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("为空队列\n");
	}
	else
	{
		printf("%d\n",Queue->Front->Next->Data);
	}
} 

//判断队列是否为空
void QueueEmpty(LinkQueue* Queue)
{
	if(Queue->Front==Queue->Rear)
	{
		printf("为空队列\n");
	}
	else
	{
		printf("队列不为空\n"); 
	} 
} 

//清空队列
void ClearQueue(LinkQueue* Queue)
{
	QNode* p=Queue->Front->Next;
	QNode* q;
	while(p)
	{
		q=p->Next;
		free(p);
		p=q;
	}                        //清空队列 
	Queue->Front->Next=NULL;//头结点的Next置为NULL 
	Queue->Rear=Queue->Front;//尾指针和头指针指向一块地方,表示队列为空。 
} 

//输出队列长度
void QueueLength(LinkQueue* Queue)
{
	int Count=0;
	QNode* Ptr=Queue->Front->Next;
	while(Ptr)
	{
		Count++;
		Ptr=Ptr->Next;
	}
	printf("%d\n",Count);
} 
int main()
{
	LinkQueue* Queue;
	Queue=(LinkQueue*)malloc(sizeof(LinkQueue));
	InitQueue(Queue);
	Push(Queue,1); 
	Push(Queue,2); 
	Push(Queue,3); 
	Push(Queue,4); 	
	Push(Queue,5);
	GetHead(Queue);
	QueueLength(Queue); 
	Pop(Queue);
	GetHead(Queue);
	QueueLength(Queue);
	QueueEmpty(Queue);
	ClearQueue(Queue);
	QueueEmpty(Queue);
	return 0;
}

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值