零基础学算法(第4版)
上QQ阅读APP看书,第一时间看更新

2.3 先进先出结构:队列

在程序设计中,队列是一种很常用的数据结构。队列在现实生活中就有对应的例子,可以认为是来源于生活。本节介绍队列的特点和操作队列的C语言代码。

2.3.1 什么是队列

队列是一种特殊的线性表,只允许在表的前端进行删除操作,而在表的后端进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。当队列中没有元素时,称为空队列。队列具有“先进先出”的特点。

在日常生活中,随处可见队列的例子。例如,去火车站买车票,每个窗口都有很多人排队买票,每个窗口就是一个队列。在这个队列中,新来的人总是排在队列的后面,排在最前面的人总是先购票,这就是“先来先服务”原则。

提示 队列这种数据结构与此相同,也是一种先进先出(First In First Out,FIFO)的结构。

对于队列这种结构,其操作比较简单,主要有以下几种常用操作。

·初始化队列:创建一个队列。

·进队列:将一个元素添加到队尾(相当于到队列最后排队等候)。

·出队列:将队头的元素取出,同时删除该元素,使下一个元素成为队头。

·获取队列第1个元素:将队头的元素取出,不删除该元素(队头仍然是该元素)。

·获取队列长度:根据队头、队尾计算出队列中元素的数量。

2.3.2 操作队列

队列是线性表的一种特殊表现形式(线性表可在表的中间部位插入或删除元素,而队列不允许这种操作),因此,其存储结构与线性表相同,也可采用顺序存储方式和链式存储方式。本节只介绍顺序存储队列的方式,有关用链式存储队列的方式可参见上节中链表的相关介绍。

将队列用顺序存储方式保存时,在内存中申请一片区域用来保存元素,如图2-17所示。当第一个元素A进入队列时,因是空列队,因此元素A将排在队头,接着B、C、D顺次进入队列,就得到如图所示的队列。其中head指向队头,以指示队头元素出队操作,tail指向队尾,以指示新增元素进入队列。

执行一次出队操作,元素A将出队,这时第2个元素B将成为队头,head指针将指向该元素,如图2-18所示。

图2-17 顺序队列

图2-18 元素A出队后的队列

如果要将元素E入队,则是将其放入tail指针指向元素的后一个位置(图2-18中序号为5的单元),并使tail指针指向序号为5的单元。

1.定义顺序队列结构

了解队列的操作后,接下来就可编写用C语言操作队列的相关函数。

【程序2-11】顺序队列操作函数“2-11 SeqQueue.h”


1    #define QUEUEMAX 15
2    typedef struct               //定义队列结构体
3    {
4        DATA data[QUEUEMAX];     //队列数组
5        int head;             //队头
6        int tail;             //队尾
7    }SeqQueue;

【程序说明】

以上代码定义一个队列结构,其中第1行设置队列的大小(即可允许多少元素进入队列),第3行使用DATA类型定义一个数组,用来保存队列中各元素内容(数据类型DATA在主调程序中定义,以方便操作不同类型的数据),第5、6行定义的变量保存队头和队尾的序号。

提示 与顺序表对比,在队列的数据结构中增加了队头和队尾的两个指针,指示队列的队头和队尾,方便程序判断操作。

2.初始化队列

在使用队列之前,首先需从系统中申请一片内存,用来保存队列中的数据,并设置队头和队尾的序号。这些操作将在初始化时完成。具体代码如下;


8    SeqQueue *SeqQueueInit()
9    {
10        SeqQueue *q;
11        if(q=(SeqQueue *)malloc(sizeof(SeqQueue)))    //申请保存队列的内存 
12        {
13            q->head = 0;                            //设置队头
14            q->tail = 0;                            //设置队尾
15            return q;
16        }else
17            return NULL;                            //返回空
18    }

【程序说明】

该函数不需要参数,但要返回一个指向列队的指针,后续代码将使用该指针操作队列。第11行申请内存,若分配内存成功,执行第13、14行设置队头队尾的序号都为0,然后返回分配的内存首地址。

由于队列由malloc函数申请内存块来保存元素的值,因此,在不使用队列时,应调用free函数显式地释放对该内存块的使用,编写以下函数用来释放队列:


19    void SeqQueueFree(SeqQueue *q)                     //释放队列所占内存
20    {
21        if (q!=NULL)
22            free(q);
23    }

编程经验 使用malloc函数申请的内存,都应在程序中显式地调用free函数将其释放。

3.获取队列状态

根据队列结构的性质,对队列的操作只能在队头(出队)或队尾(入队)进行,在进行出队/入队操作时应检查队列的状态,如进行出队操作时,应检查队列是否为空,进行入队操作时,应检查队列是否已满,这些函数的代码都很简单,具体如下:


24    int SeqQueueIsEmpty(SeqQueue *q)                //队列是否为空 
25    {
26        return (q->head==q->tail);
27    }
28    int SeqQueueIsFull(SeqQueue *q)            //队列是否已满
29    {
30        return (q->tail==QUEUEMAX);
31    }
32    int SeqQueueLen(SeqQueue *q)                //获取队列长度 
33    {
34        return(q->tail-q->head);
35    }

【程序说明】

·第24~27行判断队列是否为空,当队头head和队尾tail的序号相等时,则队列为空。

·第28~31行判断队列是否已满,当队尾tail序号等于常量QUEUEMAX,则表示队列已满,无法再进行入队操作。

·第32~35行返回队列长度,将队尾tail序号减去队头head序号即可得到队列的长度。

技巧 在队列的数据结构中定义的head和tail可方便地判断队列的状态。

4.入队操作

入队操作是队列最常用的操作,就是将元素添加到队列的尾部(由队尾tail序号决定),然后将队尾序号增加1,使其指向新增的元素。具体代码如下:


36    int SeqQueueIn(SeqQueue *q,DATA data)     //顺序队列的入队函数
37    {
38        if(q->tail==QUEUEMAX)                //判断队列是否已满
39        { 
40            printf("队列已满!\n");
41            return(0);
42        }else{                            //队列未满 
43            q->data[q->tail++]=data;        //加入队列, 修改队尾序号
44            return(1);
45        }
46    }

该函数有两个参数,其中q为指向队列的指针,data为要添加到队列中的元素。

编程经验 在入队操作时,必须判断队列是否已满,如果未进行判断就入队,可能会造成数据保存到未知内存区域。

5.出队操作

与入队操作相反,出队操作就是将队头元素取出。出队操作从队列首部取出队头元素(实际是返回队头元素的指针),然后修改队头head的序号,使其指向后一个元素(这样,原来指针指向的元素就不能被访问了,相当于被删除)。具体代码如下:


47    DATA *SeqQueueOut(SeqQueue *q)            //顺序队列的出队操作
48    {
49        if(q->head==q->tail)                //判断队列是否已空
50        {
51            printf("\n队列已空!\n");
52            return NULL;
53        }else{                            //队列未空
54            return &(q->data[q->head++]);    //返回指向队头的元素指针, 修改队头序号
55        }
56    }

该函数的返回值为一个指向队列中的元素DATA的指针。

6.获取队头元素

与出队操作不同,获取队头元素只是查看队头元素的内容,并不使其出队(即不从队列中删除该元素,也就是不修改队头指针head的值),具体代码如下:


47    DATA *SeqQueuePeek(SeqQueue *q)      //获取队头元素
48    {
49        if(SeqQueueIsEmpty(q))            //判断队列是否为空
50        {
51            printf("\n队列为空!\n");
52            return NULL; 
53        }else{                        //队列不为空
54            return &(q->data[q->head]);    //返回队头元素指针
55        }
56    }

以上就是对顺序表队列的常用操作。采用这种方式操作队列将出现一个问题,就是队列的假溢出。什么是假溢出?看下面的情况:假如有如图2-19所示的情况,tail已指向队列的最后,这时如果使用SeqQueueIn函数进行入队操作,虽然队列前面有5个空位置,但仍将显示队列已满。

为了解决这种假溢出,可有多种解决方案。

例如,可以在出队函数SeqQueueOut中增加移动元素的操作,即每次进行完出队操作后,就将队列中后面的元素向前移动,始终保持队列的第1个位置有元素,每次都从第1个位置进行出队操作。但是,这种操作由于需要移动大量数据,效率很低。

另一种方法就是将队列的首尾相连,构成一个环形,下面将介绍这种环形队列的操作。

2.3.3 循环队列的操作

将队列的首尾相连构成一个环形区域,当在第n个位置进行元素的入队操作后,下一个地址就“翻转”为1。采用这种方式的队列称为循环队列,而前面介绍的队列形式称为顺序队列。

循环队列实际存储结构仍然与顺序队列相同,只是在逻辑上将其形成如图2-20所示的环形。当第n个元素位置入队数据后,后面再进行入队操作,则又从第1个位置开始。

图2-19 队列的假溢出

图2-20 循环队列的结构

编程经验 在循环队列中,主要需要计算队尾tail和队头head的序号。

以入队操作为例,当需要将元素入队操作时可按以下步骤进行:

(1)将队尾序号tail增加1,即tail=tail+1。

(2)若tail=n+1,则tail=1。

其实,前两步可使用表达式tail=(tail+1)%n计算,即计算tail时,按n取余即可。

(3)若head=tail,即尾指针与头指针重合了,表示队列中元素已装满,则应提示溢出处理。

(4)若未溢出,则将元素入队即可。

下面列出循环队列的操作函数,其中大部分函数与顺序队列的操作相同。

【程序2-12】循环队列操作函数“2-12 CycQueue.h”


1    #define QUEUEMAX 15
2    typedef struct                                         //定义循环队列结构体
3    {
4        DATA data[QUEUEMAX];                         //队列数组 
5        int head;                                    //队头 
6        int tail;                                 //队尾 
7    }CycQueue;
8    CycQueue *CycQueueInit()                              //循环队列初始化
9    {
10        CycQueue *q;
11        if(q=(CycQueue *)malloc(sizeof(CycQueue)))     //申请保存队列的内存 
12        {
13            q->head = 0;                            //设置队头 
14            q->tail = 0;                            //设置队尾 
15            return q;
16        }else
17            return NULL;                             //返回空 
18    }
19    void CycQueueFree(CycQueue *q)                 //释放队列
20    {
21        if (q!=NULL)
22            free(q);
23    } 
24    int CycQueueIsEmpty(CycQueue *q)                //队列是否为空 
25    {
26        return (q->head==q->tail);
27    }
28    int CycQueueIsFull(CycQueue *q)                //队列是否已满
29    {
30        return ((q->tail+1)%QUEUEMAX==q->head);
31    }
32    int CycQueueIn(CycQueue *q,DATA data)            //入队操作
33    {
34        if((q->tail+1)%QUEUEMAX == q->head )
35        { 
36            printf("队列已满!\n");
37            return 0;
38        }else{
39            q->tail=(q->tail+1)%QUEUEMAX;            //求列尾序号 
40            q->data[q->tail]=data;
41            return 1;
42        }
43    }
44    DATA *CycQueueOut(CycQueue *q)                //循环队列的出队函数
45    {        
46        if(q->head==q->tail)                    //队列为空
47        {
48            printf("队列已空!\n");
49            return NULL;
50        }else{
51            q->head=(q->head+1)%QUEUEMAX;
52            return &(q->data[q->head]);
53        }
54    }
55    int CycQueueLen(CycQueue *q)                //获取队列长度 
56    {
57        int n;
58        n=q->tail-q->head;
59        if(n<0)
60            n=QUEUEMAX+n;
61        return n;
62    }
63    DATA *CycQueuePeek(CycQueue *q)             //获取队列中第1个位置的数据
64    {
65        if(q->head==q->tail)
66        {
67            printf("队列已经为空!\n");
68            return NULL; 
69        }else{
70            return &(q->data[(q->head+1)%QUEUEMAX]);
71        }
72    }

【程序说明】

从以上代码可看出,大部分代码与顺序队列中的代码相同,如队列的结构、队列初始化等函数。下面只介绍不同部分的代码。

·第28~31行判断队列是否已满,第30行判断tail的下一元素是否与head相同,若相同,则表示队列已满。若不理解可参见图2-15的示意图。

·第32~43行进行循环队列的入队操作,第34行判断队列是否已满,第39行计算tail队尾的序号(用求模运算),然后执行第40行将元素保存到队列序号为tail处。

·第44~54行进行循环队列的出队操作,第51行计算head队头的序号(用求模运算),然后执行第52行取出head所指向的元素(队头)。

·第55~62行根据tail减head后的值计算得出队列的长度。

·第63~72行获取循环队列中队头head的元素。

2.3.4 实例:银行排号程序

随着银行业务的扩展,到银行办理业务的人越来越多,经常可以见到银行营业大厅中排着长队。为了改善服务,很多银行都设置了排号系统,顾客去办理业务时,通过排号机生成一个号码,然后可坐在休息区等候排号系统按号码进行呼叫,从而不需要用户站在柜台前等候,缓解用户等候时的烦躁情绪。要求编写程序,模拟该排号系统。

提示 其实这就是一个队列的操作,可首先创建一个队列,每一位顾客通过该系统得到一个序号,程序将该序号添加到队列中(入队操作)。银行柜台工作人员处理完一个顾客的业务后,可选择办理下一顾客的业务,程序将从队列的头部获取下一位顾客的序号(出队操作)。

按此编写具体的代码如下:

【程序2-13】银行排号程序“2-13 BankQueue.c”

首先编写代码定义DATA数据类型。


1    #include <stdio.h>
2    #include <stdlib.h>
3    #include <time.h>             //引入时间头文件
4    typedef struct
5    {
6        int num;                 //顾客编号 
7        long time;            //进入队列时间 
8    }DATA;
9    #include "2-12 CycQueue.h"
10    int num;                    //顾客序号

【程序说明】

·第4~8行定义类型DATA,用来表示进入队列的数据,其中num为顾客序号,time为顾客取号的时间。

·第9行包含循环队列的头文件,表示本例通过循环队列来进行操作。

·第10行定义一个全局变量,用来保存顾客的序号,该序号将保存到DATA类型的num变量中。

提示 在该排号程序中,将有两种操作,一是顾客排队(入队),另外一个操作就是工作人员从队列中呼叫一个顾客去办理业务(出队)。

下面先编写新增顾客的函数,该函数为新到的顾客生成一个序号,并添加到队列中,具体代码如下:


11    void add(CycQueue *q)                            //新增顾客排列 
12    {
13        DATA data;
14        if(!CycQueueIsFull(q))                         //如果队列未满
15        {
16            data.num=++num;                            //保存顾客序号
17            data.time=time(NULL);                    //保存顾客排号时间
18            CycQueueIn(q,data);                        //调用入队函数
19        }
20        else                                        //若队列已满
21            printf("\n排队的人太多, 请稍候再排队!\n");    //显示提示信息
22    }

【程序说明】

在Add函数中,第14行判断若队列未满,则在第18行调用CycQueueIn函数将新顾客的编号等信息入队等候,否则就提示排队人太多。

下面是柜台工作人员呼叫下一顾客的函数:


23    void next(CycQueue *q)                 //通知下一顾客准备 
24    {
25        DATA *data;
26        if(!CycQueueIsEmpty(q))            //若队列不为空 
27        {
28            data=CycQueueOut(q);            //取队列头部的数据(出队) 
29            printf("\n请编号为%d的顾客办理业务!\n",data->num);
30        }
31        if(!CycQueueIsEmpty(q))             //若队列不为空 
32        {
33            data=CycQueuePeek(q);            //取队列中指定位置的数据 
34            printf("请编号为%d的顾客准备, 马上将为您办理业务!\n",data->num);      
35        }
36    }

【程序说明】

在next函数中,第26行判断若队列不为空,则在第28行调用CycQueueOut函数获取队列首部顾客的信息,通过第29行呼叫该编号的顾客前去办理业务。此时,该编号的顾客将从队列中被删除,接着在第31行再次判断队列若不为空,第33行调用CycQueuePeek函数获取队列首部顾客(已经是下一顾客了)的信息,并提醒该编号的顾客做好准备,下一次为其办理业务。

接下来编写主函数,根据不同的选择分别调用add或next函数来进行工作。具体代码如下:


37    int main()
38    {
39        CycQueue *queue1;
40        int i,n;
41        char select;
42        num=0;                    //顾客序号 
43        queue1=CycQueueInit();         //初始化队列 
44        if(queue1==NULL)
45        {
46            printf("创建队列时出错!\n");
47            getch();
48            return 0;
49        }
50        do{
51            printf("\n请选择具体操作:\n");
52            printf("1.新到顾客\n");
53            printf("2.下一个顾客\n");
54            printf("0.退出\n") ;
55            fflush(stdin);
56            select=getch();
57            switch(select)
58            {
59                case '1':
60                    add(queue1);
61                    printf("\n现在共有%d位顾客在等候!\n",CycQueueLen(queue1));
62                    break;
63                case '2':
64                    next(queue1);
65                    printf("\n现在共有%d位顾客在等候!\n",CycQueueLen(queue1));
66                    break;
67                case '0':
68                    break;
69            }        
70        }while(select!='0');
71        CycQueueFree(queue1);         //释放队列
72        getch();
73        return 0;
74    }

【程序说明】

主函数代码较多,不过逻辑很简单,首先在第43行初始化一个队列;接着在第51~54行显示一个菜单,用来模拟顾客排列或工作人员处理下一顾客业务;第57~68行根据用户的模拟选择,分别调用不同的函数进行操作;第50~70行为一个循环,当退出该循环后,表示操作完成;第71行调用CyeQueueFree函数释放队列所占用的内存空间。

编译执行以上代码,在菜单提示下输入1,表示有1位新顾客到来排队,再输入一次1,表示又有1位新顾客到来排队(这时已有2位顾客在排队等候)。这时,输入一次2,表示银行柜台开始办理业务,将显示呼叫1号顾客办理业务,并提示2号顾客做准备,执行结果如图2-21所示。

图2-21 【程序2-13】执行结果