数据结构:队列
和栈类似,队列也是一个特殊的线性表。队列在计算机系统中也有着广泛的应用。例如,在操作系统中,队列可以用于进程调度,确保按照先来先服务的原则进行任务分配。在网络通信中,队列可以用于缓存数据包,以平衡发送和接收之间的速度差异。在计算机算法中,队列可以用于广度优先搜索等算法的实现。
队列是一个很基础的数据结构,因此也需掌握。
一、简介
队列(Queue)是限定在表头删除、表尾插入的线性表。对于队列来说,允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。
队列的操作具有先进先出的特点。因此,队列又被称作FIFO(First In First Out)表。假设队列Q=(, , …, ),若队列中元素是按, , …, 的顺序依次入队的,则称为队头元素,为队尾元素。其出队次序只能按, , …, 的次序进行,也就是说,只有在, , …, 都离开队列之后,才能出队。队列的这个特点,和日常生活中的排队场景是一致的,最早进入队的最先离开。
同样地,队列也有两种存储结构,分别是顺序队列和链式队列。
二、顺序队列
顺序队列,是指采用顺序存储结构实现的队列。
在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队头到队尾的元素之外,还需附设两个整型变量front和rear分别指示队头元素及队尾元素的位置。通常,front被称为头指针,rear被称为尾指针。
在C语言中,顺序队列的类型描述如下:
1 |
|
为了描述方便起见,在此约定:初始化创建空队列时,令front=rear=0,每当插入新的队尾元素时,尾指针rear增1;每当删除队头元素时,头指针front增1。因此,在非空队列中,头指针始终指向队头元素,而尾指针始终指向队尾元素的下一个位置,其结构如下图所示:
假设当前队列的最大空间为6,则当队列处于上图(d)所示的状态时,就不可再继续插入新的队尾元素,否则会出现溢出现象,即因数组越界而导致程序的非法操作错误。事实上,此时队列的实际可用空间并未占满,所以这种现象称为“假溢出”。这是由“队尾入队,队头出队”这种受限制的操作造成的。
那么,如何解决这种“假溢出”问题呢?一个较巧妙的办法是将顺序队列变为一个环状的空间,使之成为一个循环队列。
和顺序队列一样,在循环队列中,头、尾指针以及队列元素之间的关系不变。只是,头、尾指针增1的操作用“模”运算来实现。通过取模,头指针和尾指针就可以在顺序表空间内以头尾衔接的方式“循环”移动。
例如,在下图(a)中,队头元素是J5,在元素J6入队之前,Q.rear的值为5,而当元素J6入队之后,通过“模”运算Q.rear=(Q.rear+1)%6
,得到Q.rear的值为0。这样就不会出现“假溢出”现象。
上图(b)是J1、J2、J3、J4相继进入图(a)所示队列后的状态:队列空间被占满,此时头、尾指针相同。上图(c)则是J5、J6相继从图(a)所示的队列中出队后的状态:队列为空,头、尾指针的值也是相同的。
由此可见,对于循环队列不能以头、尾指针的值是否相同来判别队列空间是“满”还是“空”。在这种情况下,如何区别队满还是队空呢?通常有以下两种处理方法:
- 少用一个元素空间。当队列空间大小为m时,有m-1个元素就认为是队满。这样判断队空的条件不变,即当头、尾指针的值相同时,则认为队空;而当尾指针在循环意义上加1后是等于头指针,则认为队满。因此,在循环队列中队空和队满的条件是:
- 队空的条件:
Q.rear==Q.front
- 队满的条件:
(Q.rear+1)%MAXQSIZE==Q.front
。如上图(d)是J7、J8、J9进入图(a)所示的队列后d的状态,(Q.rear+1)%MAXQSIZE
的值等于Q.front,此时认为队满。
- 队空的条件:
- 另设一个标志位。就是单独设置一个区别队列是“空”还是“满”的标志位。
一般情况下,实际使用的顺序队列是循环队列。循环队列的类型定义和顺序队列的类型定义是相同的。下面,以“少用一个元素空间”的方法为例,实现循环队列的一些操作。
2.1、初始化
循环队列的初始化操作就是动态分配一个预定义大小为MAXQSIZE的数组空间。该算法步骤为:
- 为队列分配一个最大容量为MAXQSIZE的数组空间,base指向数组空间的首地址。
- 头指针和尾指针置为零,表示队列为空。
相应的算法描述为:
1 |
|
2.2、求队列长度
对于非循环队列,尾指针和头指针的差值便是队列长度,而对于循环队列,差值可能为负数,所以需要将差值加上MAXQSIZE,然后与MAXQSIZE求余。相应的算法描述为:
1 |
|
2.3、入队
入队操作是指在队尾插入一个新的元素。该算法步骤为:
- 判断队列是否满,若满则返回ERROR。
- 将新元素插入队尾。
- 队尾指针加1。
相应的算法描述为:
1 |
|
2.4、出队
出队操作是将队头元素删除。该算法步骤为:
- 判断队列是否为空,若空则返回ERROR。
- 保存队头元素。
- 队头指针加1。
相应的算法描述为:
1 |
|
2.5、取队头元素
当队列非空时,返回当前队头元素的值,队头指针保持不变。相应的算法描述为:
1 |
|
三、链式队列
链式队列,是指采用链式存储结构实现的队列。
链式队列通常用单链表来表示。一个链式队列需要两个指针,分别指示队头和队尾才能唯一确定。其中,指向队头的指针称为头指针,指向队尾的指针称为尾指针。
和单链表类似,为了简化边界条件的处理,会给链式队列添加一个头结点,并使头指针指向该结点。
在C语言中,链式队列的类型描述如下:
1 |
|
链式队列的操作是单链表插入和删除操作的特殊情况。下面给出链队初始化、入队、出队操作的实现。
3.1、初始化
链队的初始化操作就是构造一个只有头结点的空队列。
该算法步骤为:
- 生成新结点作为头结点,队头和队尾指针指向此结点。
- 头结点的指针域置空。
相应的算法描述为:
1 |
|
3.2、入队
和循环队列的入队操作不同,链队在入队前不需要判断队是否满,仅需为入队元素动态分配一个结点空间。
该算法步骤为:
- 为入队元素分配结点空间,用指针p指向。
- 将新结点数据域置为e。
- 将新结点插入到队尾。
- 修改队尾指针为p。
相应的算法描述为:
1 |
|
3.3、出队
和循环队列一样,链队在出队前也需要判断队列是否为空,不同的是,链队在出队后需要释放队头元素的所占空间。
该算法步骤为:
- 判断队列是否为空,若空则返回ERROR。
- 临时保存队头元素的空间,以备释放。
- 修改头结点的指针域,使其指向下一个结点。
- 判断出队元素是否为最后一个元素,若是,则将队尾指针指向头结点。
- 释放原队头元素的空间。
相应的算法描述为:
1 |
|
3.4、取队头元素
与循环队列一样,当队列非空时,此操作返回当前队头元素的值,队头指针保持不变。相应的算法描述为:
1 |
|
四、小结
队列也是一种操作受限的线性表,它只允许在表头进行删除操作、在表尾进行插入操作。队列最大的特点是先进先出,因此也被称为FIFO表。队列有两种存储结构,分别是顺序队列和链式队列。队列的主要操作是进队和出队,对于顺序队列(更准确地来说是循环队列)的进队和出队操作要注意判断队满或队空。凡是涉及队头或队尾指针的修改都要将其对MAXQSIZE求模。