1.队列的概念
在开始前,请牢记这句话:队列是一个先进先出的数据结构。
队列(queue)是限定在表的一端进行插入,表的另一端进行删除的数据结构,如同栈的学习,请联系前文所学链表,试想一个单链表,我们只能对他的链表表尾进行插入,而只能对链表的表头进行结点的删除,其余一切的操作均不允许,这样强限制性的“链表“,就是我们所说的队列。
让我们重新理顺一下定义:队列是一个线性的数据结构,规定这个数据结构只允许在一端进行插入,另一端进行删除,禁止直接访问除这两端以外的一切数据。
如图:队列就像一个两端相通的水管,只允许一端插入,另一端取出,先放入管中的球先从管中拿出。
2.队列的结点设计与初始化
队列不如栈那般方便进行模拟,因此队列只有链式的设计方法,但是不同的是,队列本身分为多种队列,如顺序队列和循环队列。
我们以顺序队列的设计为例。
首先是队列的结点设计,我们可以设计出两个结构体,一个结构体Node表示结点,其中包含有一个data域和next指针(可以发现,这样的结点设计我们已经重复出现了很多次了,正是因为这样的结构设计方便理解)。
其中data表示数据,其可以是简单的类型(如int,double等等),也可以是复杂的结构体(struct类型)。
next指针表示,下一个的指针,其指向下一个结点,通过next指针将各个结点链接。
如同栈再次设计一个结构体进行限制性设计,接着,我们也额外添加一个结构体,其包括了两个分别永远指向队列的队尾和队头的指针,我们主要的操作只对这两个指针进行操作。
其结构体设计的代码可以表示为:
//结点定义
typedef struct node{
int data;
struct node *next;
}node;
//队列定义,队首指针和队尾指针
typedef struct queue{
node *front; //头指针
node *rear; //尾指针
}queue;
有关初始化,稍微复杂一点的是,我们对于初始化需要初始化两个类型,一个是初始化结点,一个是初始化队列,初始化队列稍微有些不同,那就是当初始化队列的时候,需要将头尾两个结点指向的内容统统制空,表示是一个空队列,两个创建的函数代码可以表示为:
//初始化结点
node *init_node(){
node *n=(node*)malloc(sizeof(node));
if(n==NULL){ //建立失败,退出
exit(0);
}
return n;
}
//初始化队列
queue *init_queue(){
queue *q=(queue*)malloc(sizeof(queue));//malloc默认返回void*类型
if(q==NULL){ //建立失败,退出
exit(0);
}
//头尾结点均赋值NULL
q->front=NULL;
q->rear=NULL;//空队列判断头尾是否为空
return q;
}
3.判断队列是否为空
判断队列是否为空比较简单,直接就是判断队列头指针是否是空值即可(关联如何创建队列可更好理解),判断队列是否为空是比较常用的操作。
其代码可以表示为:
//队列判空
int empty(queue *q){
if(q->front==NULL){
return 1; //1--表示真,说明队列非空
}else{
return 0; //0--表示假,说明队列为空
}
}
或者直接利用返回值进行更简单的判断(两者效果完全一样)
int empty(queue *q){
return q->front==NULL;
}
4.队列的基本操作
4.1入队操作
如图,进行入队(push)操作的时候,我们首先需要特判一下队列是否为空,如果队列为空的话,需要将头指针和尾指针一同指向第一个结点,即front=n;rear=n。
当如果队列不为空的时候,我们只需要将尾结点向后移动,通过不断移动next指针指向新的结点构成队列即可。
其代码可以表示为:
//入队操作
void push(queue *q,int data){
node *n =init_node();
n->data=data;
n->next=NULL; //采用尾插入法
//if(q->rear==NULL){ //使用此方法也可以
if(empty(q)){ //检查队列是否为空
q->front=n;
q->rear=n;
}else{
q->rear->next=n; //n成为当前尾结点的下一结点
q->rear=n; //让尾指针指向n
}
}
4.2出队操作
出队(pop)操作,是指在队列不为空的情况下(请注意一定要进行队列判空的操作),进行一个判断,如图,如果队列只有一个元素了(即头尾指针均指向了同一个结点),直接将头尾两指针制空(NULL)并释放这一个结点即可。
如图,当队列含有二以上个元素时,我们需要将队列的头指针指向头指针当前指向的下一个元素并释放掉当前元素即可。
其代码可以表示为:
//出队操作
void pop(queue *q){
node *n=q->front;
if(empty(q)){
return ; //此时队列为空,直接返回函数结束
}
if(q->front==q->rear){
q->front=NULL; //只有一个元素时直接将两端指向制空即可
q->rear=NULL;
free(n); //记得归还内存空间
}else{
q->front=q->front->next;
free(n);
}
}
4.3打印队列元素(遍历)
打印队列的全部元素是在队列不为空的情况下,通过结点的next指向依次遍历并输出元素既可以。
其代码可以表示为:
//打印队列元素
void print_queue(queue *q){
node *n = init_node();
n=q->front;
if(empty(q)){
return ; //此时队列为空,直接返回函数结束
}
while (n!=NULL){
printf("%d\t",n->data);
n=n->next;
}
printf("\n"); //记得换行
}
5.循环队列及溢出现象
5.1顺序队列的假溢出&循环队列的概念
我们已经明白了队列这种基本数据结构,对于顺序队列而言,其存在已经足够解决大多时候的设计问题了,但是其依旧存在一些缺陷和不足,因为我们的入队和出队操作均是直接在其后面进行结点的链接和删除,这就造成其使用空间不断向出队的那一边偏移,产生假溢出。
什么是假溢出?打一个比方:
回顾一下队列的性质,首先我们有一个顺序队列,这个队列的大小为5,其已经包含了四个元素data1,data2,data3,data4,接着,我们对这个队列进行出队操作,出队2个元素,队列就变成了这个样子:
目前看起来没有问题,那么我们接着再进行入队操作,我们入队2个元素,分别是data5和data6,此时我们已经发现问题了,尾指针移动到我们可以进行队列操作的范围之外去了,我们称呼作为队列用的存储区还没有满,但队列却发生了溢出,我们把这种现象称为"假溢出"。
可能这个时候会产生一个疑问,我们学习的队列不是使用链表实现的动态队列么?没有空间的时候会开辟空间,这难道还会产生假溢出么?
是的的确,当进行动态创建队列的时候,也只不过是向后继续不断的申请内存空间,即时前面出队操作释放掉了前面的空间,但是指针依旧会向后进行移动,直到达到系统预留给程序的内存上界被强行终止,这对于极为频繁的队列操作和程序而言是致命的,这时候,就需要对我们的队列进行优化,使用更为优秀的结构——循环队列。
循环队列的思维非常简单,就是给定我们队列的大小范围,在原有队列的基础上,只要队列的后方满了,就从这个队列的前面开始进行插入,以达到重复利用空间的效果,由于循环队列的设计思维更像一个环,因此常使用一个环图来表示,但注意其不是一个真正的环,循环队列依旧是单线性的。
5.2循环队列的结构设计
由于循环对列给定了数据范围的大小,则不需要使用链式的动态创建方法了(如果依旧使用链式存储,会无法确定何时再回到队头进行插入操作),因此我们采用模拟的方法,如图所示:
其中,data表示一个数据域,int为类型,其可以修改为任意自定义的类型,比如说简单的char,float类型等等,也可以是复杂的结构体类型,我们使用maxsize表示循环队列的最大容纳量,其表示队列的全部可操作空间。
rear代表尾指针,入队时移动。
front代表头指针,出队时移动。
其代码可以表示为:
#define maxsize 10 //表示循环队列的最大容量
//循环队列的结构设计
typedef struct cir_queue{
int data[maxsize];
int rear;
int front;
}cir_queue;
5.3循环队列初始化
我们初始化相比链表而言更为简单了,核心就在于申请空间以及将front指针和rear指针内容赋值为0,即指向第0个元素即可(注意第 0个元素内容为空)。
其代码可以表示为:
//初始化
cir_queue *init(){
cir_queue *q = (cir_queue*)malloc(sizeof(cir_queue));
if(q==NULL){
exit(0); //申请内存失败,退出程序
}
q->front=0;
q->rear=0;
return q;
}
5.4循环队列入队操作
入队操作同顺序队列的方法,直接将rear向后移动即可,但是要注意判断,如果rear达到了队列的空间上线,将要从头继续开始移动,这里推荐使用余数法,即无论如何求余都是在这片空间内进行操作,防止一次错误执行就直接整体崩溃,而且也相对而言更为简洁,不推荐使用if语句,这样显得比较累赘。
注意进行加一移动位置操作的时候,不能直接q->rear++这样的操作,这样计算机判断优先级会产生让自己意想不到的后果,此外这里还需要进行一次是否队列已满的判断,当我们rear指针的下一个位置就是front的位置的时候,即改循环队列已满。
其代码可以表示为:
//入队操作push
void push(cir_queue *q,int data){
if((q->rear+1)%maxsize==q->front){
printf("溢出,无法入队\n");
return;
}else{
q->rear=(q->rear+1)%maxsize;
q->data[q->rear]=data;
}
}
5.5循环队列出队操作
如果顺序队列的出队操作,直接将front进行后移一位即可,注意这时候有一个需要留意的地方,即队列是否为空,当队列为空的时候是无法进行出队操作的。
其代码可以表示为:
//出队操作pop
void pop(cir_queue *q){
if(q->rear==q->front){
printf("队列为空,无法出队\n");
return;
}else{
q->data[q->front]=0;
q->front=(q->front+1)%maxsize;
}
}
5.6循环队列遍历操作
遍历操作需要借助一个临时变量储存位置front的位置信息,利用i逐步向后移动,直到i到达了rear的位置即可宣告遍历的结束。
//遍历队列
void print(cir_queue *q){
int i=q->front;
while(i!=q->rear){
i=(i+1)%maxsize;
printf("%d\t",q->data[i]);
}
printf("\n"); //记得换行
}