一、基本概念
1、队列的定义
队列 是仅限在 一端 进行 插入,另一端 进行 删除 的 线性表。
队列 又被称为 先进先出 (first in first out) 的线性表,简称 FIFO 。
2、 队首
允许进行元素删除的一端称为 队首。
3、 队尾
允许进行元素插入的一端称为 队尾。
二、队列的存储结构
队列是操作受限的线性表,那么它在存储结构上也同线性表相同,主要分为两种基本的存储结构:顺序存储结构 和 链式存储结构 ;
1、循环队列
循环队列 可以理解为 队列的顺序表实现
(1)、循环队列的定义
对于顺序表,在 C语言 中表现为 数组,在进行 队列 的定义 之前,我们需要考虑以下几个点:
- 1)队列数据的存储方式,以及队列数据的数据类型;
- 2)队列的大小;
- 3)队首指针;
- 4)队尾指针;
因此,其代码实现如下:
#define int elemtype // (1)
#define MAXSIZE 1024 // (2)
typedef struct SequenQueue {
elemtype data[MAXSIZE]; // (3)
int front, rear; // (4)
}SequenQueue; // (5)
- (1) 用
elemtype
这个宏定义来统一代表队列中数据的类型,这里将它定义为int
,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等; - (2)
MAXSIZE
代表我们定义的队列的最大元素个数; - (3)
elemtype data[MAXSIZE]
作为队列元素的存储方式,即 数组,数据类型为elemtype
; - (4)
front
即,队首指针,rear
即队尾指针;当头尾指针所指相同时,即,front
==rear
代表空队;当队列非空时,data[front]
代表了队首元素,与栈相似,队尾元素是不需要关心的; - (5)
SequenQueue
就是我们接下来会用到的 队列结构体;
在循环队列中,由于其循环的特性,无法仅凭
front == rear
就判别该队列是 “满” 还是 “空” ,因此,产生了以下两种解决方案。
(1)、少用一个数据元素的空间,当队尾指针所指的空闲单元的后继单元是队头元素所在的单元时,认为该队列已满,不能插入新的数据元素。此时,循环队列的 判满条件 为(Q.rear +1 )%MAXSIZE == Q
;队列 判空条件 为Q.rear == Q.front
;
(2)、设置一个计数器ans
并置为 0 ,每当有新的数据元素入队时,计数器自增;有新的数据元素出队时,计数器自减。当计数器的数值等于 MAXSIZE 时,表示队列已满,不能再入队;当计数器的数值等于 0 时,表示队列为空,不能再出队。(一般计数器会导致一些程序出现瑕疵,所以多采用法一。)
(2)、循环队列的基本运算
初始化
算法描述
队列的初始化,即构造一个空队列。
可以先声明一个循环队列,先申请动态分配内存空间,并将Q的队头和队尾指针都置为 0 ,使得队列没有数据元素,从而达到空队列的效果。
代码示例
SequenQueue * Init_SequenQueue()
{
SequenQueue *Q; //(1)
Q = (SequenQueue * ) malloc (sizeof (SequenQueue)); //(2)
if(Q != NULL) //(3)
{
Q->front = 0; //(4)
Q->rear = 0;. //(5)
}
return Q;
}
代码注释
- (1) 声明并定义循环队列指针变量;
- (2) 利用系统函数
malloc
函数,申请分配内存空间; - (3) 判断是否申请空间成功;
- (4) 设置循环队列的头指针为初始值
front = 0
; - (5) 设置循环队列的尾指针为初始值
rear = 0
;
循环队列索引
算法描述
队列的长度,即循环队列Q中所包含数据元素的个数,可以根据头、尾指针的数值结合组城的最大长度来计算。简单地说,因为只有在 入队 的时候,队尾指针 加一;出队 的时候,队首指针 加一;所以 队列元素个数 就是两者的差值;
代码示例
int SequenQueue_Size (SequenQueue *Q)
{
return Q->rear - Q->front ;
}
如果采用 (1)、循环队列的定义 所述的方法,也同样可以做出,其代码如下 :
// 法一
int SequenQueue_Size (SequenQueue *Q)
{
if( (Q->rear +1) %MAXSIZE == Q->front) //(1)
return 1;
else
return 0;
}
// 法二
int SequenQueue_Size (SequenQueue *Q)
{
if(Q->size == MAXSIZE ) //(2)
return 1;
else
return 0;
}
- 法一、法二具体解释详见 (1)、循环队列的定义 ,这里便不再累述;
队列判空
算法描述
当队列元素个数为零时,就是一个 空队,空队 不允许 出队 操作。因此这里可以调用 队列索引函数,判断其长度是否为 0;
代码示例
int SequenQueue_Empty(SequenQueue *Q)
{
return !SequenQueue_Size(Q);
}
若不想调用其他函数,也可以直接判断,代码如下:
int SequenQueue_Empty(SequenQueue *Q)
{
if(Q->front == Q->rear) //(1)
return 1;
else
return 0;
}
代码注释
- (1) 由于循环队列具有循环性,因此检查头尾指针的值,观察其是否相等即可。(这里推荐用第一种思路)
队列判满
算法描述
当队列元素个数为 MAXSIZE
时,就是一个 满队,满队 不允许 入队 操作。举一反三,如 判空 思路相似,我们同样可以调用 队列索引 函数,判断其长度是否为 MAXSIZE 。
代码示例
int SequenQueue_Full (SequenQueue *Q)
{
retuurn !SequenQueue_Size(Q)%MAXSIZE ;
}
触类旁通,如 判空 一样,如果不调用其他函数,那么思路便如 (1)、循环队列的定义 中所叙述,代码如下:
int SequenQueue_Full (SequenQueue *Q)
{
if( (Q->rear +1)%MAXSIZE == Q->front);
return 1;
else
return 0;
}
入队
算法描述
队列的 插入 操作,叫做 入队。它是将 数据元素 从 队尾 进行插入的过程。形象地说,就是排队时,每个人去都是都队伍后面排队的,这样的方式就叫 入队。
代码实现
int Push_SequenQueue(SequenQueue *Q,elemtype x) //(1)
{
if(SequenQueue_Full(Q)) //(2)
{
return 0;
}
Q->data[Q->rear] = x; //(3)
Q->rear = (Q->rear +1)%MAXSIZE; //(4)
return 1;
}
代码注释
- (1)
Q
是一个指向队列对象的指针,由于这个接口会修改队列对象的成员变量,所以这里必须传指针,否则,就会导致函数执行完毕,传参对象没有任何改变; - (2) 检查队列是否满队,若满队,则无法插入;
- (3) 在队列队尾中插入
x
; - (4) 将队列尾指针加一;
这里我认为有个小偷懒的方法,将(3) 和 (4) 合并成一句话;
int Push_SequenQueue(SequenQueue *Q, elemtype)
{
if(SequenQueue_Full(Q))
return 0;
Q->data[ Q->rear++ ] = x;
return 1;
}
Q->rear++
表达式的值是自增前的值,并且自身进行了一次自增。
出队
算法描述
队列的删除操作,叫做 出队。它是将 队首 元素进行 删除 的过程,就像排队,从队尾开始进入,那么一定是从队首出去。
代码示例
int Pop_SequenQueue(SequenQueue *Q)
{
if(SequenQueue_Empty) //(1)
return 0;
Q->front = (Q->front +1) %MAXSIZE ; //(2)
return 1;
}
- (1) 检查队列是否为空队,若为空队,则无法出队。
- (2) 修改头指针
front
的数值;
取队头数据元素
算法描述
同栈的做法相同,只需要将循环队列的队首数据元素返回即可;
代码实现
elemtype GetFront_SequenQueue(SequenQueue *Q,elemtype)
{
if(Q->front == Q-> rear) //(1)
return 0;
else
{
*x = Q->data[Q->front]; //(2)
return 1;
}
}
代码注释
- (1) 检查队列是否为空,若为空队列,则无数据元素,无法读取;
- (2) 将队头数据元素赋值给
x
;
这里,如果熟练了,同样可以再写短一些;
具体代码如下:
elemtype GetFront_SequenQueue(SequenQueue *Q)
{
if(SequenQueue_Empty(Q))
return 0;
return Q->data[Q->front];
}
(2)、优缺点
1) 优点
与顺序栈类似,在利用顺序表实现队列时,入队 和 出队 的常数时间复杂度低
2) 缺点
需要预先申请好空间,而且当空间不够时,需要进行扩容。
个人见解,可以使用 vector
扩容
详细请见 👉👉👉 vector 扩容
2、链式队列
(1)、循环队列的定义
同链式栈相似,对于链表,在进行 队列的定义 之前,我们需要考虑以下几个点:
- 1)队列数据的存储方式,以及队列数据的数据类型;
- 2)队列的大小;
- 3)队首指针;
- 4)队尾指针;
因此 链式队列 的 结构体 定义如下:
typedef int elemtype ; //(1)
typedef struct QueueNode;
{
elemtype data; //(2)
struct QueueNode *next; //(3)
}LinkQueueNode; //(4)
typedef struct LQueue
{
LinkQueueNode *front,*rear; //(5)
int size //(6)
}LQueue,*LinkQueue; //(7)
- (1)
elemtype
表示 队列结点元素的 数据域 ,这里定义为int
类型 ; - (2)
elemtype data
代表 数据域 ,用来存放数据元素信息; - (3)
struct QueueNode *next
代表 指针域 ,用来表示指向下一个结点的指针; - (4)
LinkQueueNode
是链式队列结点的结构类型; - (5)
front
代表 队首指针 ;rear
代表 队尾指针 ; - (6)
size
用来表示队列中数据元素的 个数 。在 链式队列 中,求链长的时间复杂度是 O(n) ,所以可以用size
来进一步优化程序,每次 入队 时size
自增,出队时size
自减。这样在询问队列的 长度 的时候,就可以通过 O(1) 的时间复杂度。 - (7) 链式队列的结构类型
(2)、链式队列的基本运算
初始化
算法描述
链式队列的初始化,就是构造一个空队列。将头指针 front
和尾指针 rear
都指向头结点,并置头结点的指针域为空,使得该队列中不存在任一数据元素,从而达到初始化的目的。
代码示例
LinkQueue Init_LinkQueue()
{
LinkQueue Q= new LQueue; //(1)
LinkQueueNode *head = (LinkQueueNode *) malloc (sizeof(LinkQueueNode)); //(2)
if(head != NULL && Q!=NULL) //((3)
{
head -> next = NULL; //(4)
Q->front = head; //(5)
Q->rear = head; //(6)
}
return Q;
}
代码解释
- (1) 为链式队列的队首、队尾指针申请分配动态内存空间;
- (2) 利用
malloc
函数为链队列的头结点申请分配动态内存空间; - (3) 判断是否分配空间成功,若是则进行(4) - (6) ;
- (4) 设置 队首指针 指向 头结点;
- (5) 设置 队尾指针 指向 头结点;
- (6) 返回链式队列指针;
链式队列索引
算法描述
由于在设置结构体时,已经设置了 size
用于记录该链式队列的元素个数,因此直接返回 size
大小即可;
代码实现
elemtype LinkQueue_Size(LinkQueue Q)
{
return Q->size;
}
队列判空
算法描述
当该链式队列的元素个数为 0 时,该队列为空队列;
另外,正如初始化一样,因为是链式队列,所以当队首指针和队尾指针都指向同一个头结点时,也认为该队列为空队列。
两种思路的代码实现如下 :
代码实现
// 思路一
elemtype LinkQueue_Empty(LinkQueue Q)
{
return Q->size;
}
// 思路二
elemtype LinkQueue_Empty(LinkQueue Q)
{
if(Q->front == Q->rear)
return 1;
else
return 0;
}
入队
算法描述
入队 操作,其实就是类似 尾插法,往链表尾部插入一个新的结点。思路原理类似于循环队列的入队;
另外,在插入数据元素之后,需要将队尾指针指向心如对的队尾数据元素。
代码实现
elemtype LinkQueue(LinkQueue Q,elemtype x)
{
LinkQueueNode *node; //(1)
node = (LinkQueueNode *) malloc (sizeof(LinkQueueNode)); //(2)
if(node == NULL)
return 0; //(3)
node->data = x; //(4)
node->next = NULL; //(5)
Q->rear->next = node; //(6)
Q->rear = node; //(7)
++Q->size; //(8)
return 1;
}
代码解释
- (1) 定义链式队列结点的指针变量;
- (2) 利用
malloc
函数 为 结点 申请分配内存空间; - (3) 若申请结点空间失败,则返回 0;
- (4) 将需要入队的数据
x
填充到新的结点node
; - (5) 设置新的结点
node
的指针域为空; - (6) 将新节点插入队尾;
- (7) 设置队尾指针指向新的队尾元素;
- (8) 队列元素计数器
size
自增;
出队
算法描述
出队就是删除队列队首的数据元素,并将头结点指向链式队列的下一个数据元素;需要注意的时,如果队列为空,则无法进行出队操作!
代码实现
int Pop_LinkQueue(LinkQueue Q )
{
LinkQueueNode *node; //(1)
if(LinkQueue_Empty(Q)) //(2)
return 0;
node = Q->front->next; //(3)
Q->front->next = node->next; //(4)
free(node); //(5)
--Q->size; //(6)
if(Q->size == 0) //(7)
Q->rear = NULL; //(8)
return 1;
}
代码分析
- (1) 定义链式队列结点的指针变量;
- (2) 通过调用函数
LinkQueue_Empty
来判断该链式队列是否为空 ,若为空则出队失败,返回 0 ; - (3) 将员队列队首的数据元素弹出,并赋给
node
- (4) 将队首指针
Q
指向链式队列的下一个数据元素,从而实现将 队首 的 后继结点 作为新的 队首 ; - (5) 释放之前的 队首 对应的内存;
- (6) 队列数据元素计数器
size
自减; - (7) 判断该队列出队之后是否为空队列;
- (8) 若该队列为空队列,则将 队尾指针 置空;
取队头数据元素
算法描述
思路同之前的其他结构读取首个数据元素相同;
代码实现
elemtype GetFront_LinkQueue(LinkQueue Q,elemtype *x)
{
if(LinkQueue_Empty(Q))
return 0;
else
{
*x = Q->front->next->data;
return 1;
}
}
优缺点
优点
不需要预先分配空间,且在内存允许范围内,可以一直 入队,不受顺序表总容量的限制。
缺点
在利用链表实现队列时,入队 和 出队 的常数时间复杂度略高,主要是每插入一个队列元素都需要申请空间,每删除一个队列元素都需要释放空间,且 清空队列 操作是 O(n) 的,直接将 队首指针 和 队尾指针 置空会导致内存泄漏。
后序
需要注意的是,本文在讲解过程中,循环队列 的 队尾 和 链式队列 的 队尾 不是一个概念,循环队列 的 队尾 没有实际元素值,而 链式队列 的队尾则不然,请细品!
活学活用
用两个容量分别为O和P(O>P) 的栈模拟一个队列,那么模拟实现的队列最大容量为多少?
答案:2P+1;
解析:首先需要保证栈转换成队列的合法性,双栈实现队列的时候入队的那个栈必须一次把所有元素都挪到出对的那个栈,否则影响出队顺序。如果把O作为出队的栈元素最多只能是2P,如果是P作为出队的栈,可以先让O放P个元素到P栈里自己再放O个。这时候你再想出队顺序合法就只能是让O存P+1个,上面P个放到P栈,O剩下的一个是现在应该出队的元素
解析源于 爱吃秋刀鱼