队列 是仅限在 一端 进行 插入,另一端 进行 删除 的 线性表。
队列 又被称为 先进先出 (first in first out) 的线性表,简称 FIFO 。
允许进行元素删除的一端称为 队首。
允许进行元素插入的一端称为 队尾。
队列是操作受限的线性表,那么它在存储结构上也同线性表相同,主要分为两种基本的存储结构:顺序存储结构 和 链式存储结构 ;
循环队列 可以理解为 队列的顺序表实现
对于顺序表,在 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)
elemtype
这个宏定义来统一代表队列中数据的类型,这里将它定义为 int
,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等;MAXSIZE
代表我们定义的队列的最大元素个数;elemtype data[MAXSIZE]
作为队列元素的存储方式,即 数组,数据类型为elemtype
;front
即,队首指针,rear
即队尾指针;当头尾指针所指相同时,即,front
== rear
代表空队;当队列非空时,data[front]
代表了队首元素,与栈相似,队尾元素是不需要关心的;SequenQueue
就是我们接下来会用到的 队列结构体;在循环队列中,由于其循环的特性,无法仅凭
front == rear
就判别该队列是 “满” 还是 “空” ,因此,产生了以下两种解决方案。
(1)、少用一个数据元素的空间,当队尾指针所指的空闲单元的后继单元是队头元素所在的单元时,认为该队列已满,不能插入新的数据元素。此时,循环队列的 判满条件 为(Q.rear +1 )%MAXSIZE == Q
;队列 判空条件 为Q.rear == Q.front
;
(2)、设置一个计数器ans
并置为 0 ,每当有新的数据元素入队时,计数器自增;有新的数据元素出队时,计数器自减。当计数器的数值等于 MAXSIZE 时,表示队列已满,不能再入队;当计数器的数值等于 0 时,表示队列为空,不能再出队。(一般计数器会导致一些程序出现瑕疵,所以多采用法一。)
队列的初始化,即构造一个空队列。
可以先声明一个循环队列,先申请动态分配内存空间,并将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;
}
malloc
函数,申请分配内存空间;front = 0
;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;
}
当队列元素个数为零时,就是一个 空队,空队 不允许 出队 操作。因此这里可以调用 队列索引函数,判断其长度是否为 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;
}
当队列元素个数为 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;
}
Q
是一个指向队列对象的指针,由于这个接口会修改队列对象的成员变量,所以这里必须传指针,否则,就会导致函数执行完毕,传参对象没有任何改变;x
;这里我认为有个小偷懒的方法,将(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;
}
front
的数值;同栈的做法相同,只需要将循环队列的队首数据元素返回即可;
elemtype GetFront_SequenQueue(SequenQueue *Q,elemtype)
{
if(Q->front == Q-> rear) //(1)
return 0;
else
{
*x = Q->data[Q->front]; //(2)
return 1;
}
}
x
;这里,如果熟练了,同样可以再写短一些;
具体代码如下:
elemtype GetFront_SequenQueue(SequenQueue *Q)
{
if(SequenQueue_Empty(Q))
return 0;
return Q->data[Q->front];
}
与顺序栈类似,在利用顺序表实现队列时,入队 和 出队 的常数时间复杂度低
需要预先申请好空间,而且当空间不够时,需要进行扩容。
个人见解,可以使用 vector
扩容
详细请见 vector 扩容
同链式栈相似,对于链表,在进行 队列的定义 之前,我们需要考虑以下几个点:
- 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)
elemtype
表示 队列结点元素的 数据域 ,这里定义为 int
类型 ;elemtype data
代表 数据域 ,用来存放数据元素信息;struct QueueNode *next
代表 指针域 ,用来表示指向下一个结点的指针;LinkQueueNode
是链式队列结点的结构类型;front
代表 队首指针 ; rear
代表 队尾指针 ;size
用来表示队列中数据元素的 个数 。在 链式队列 中,求链长的时间复杂度是 O(n) ,所以可以用 size
来进一步优化程序,每次 入队 时 size
自增,出队时 size
自减。这样在询问队列的 长度 的时候,就可以通过 O(1) 的时间复杂度。链式队列的初始化,就是构造一个空队列。将头指针 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;
}
malloc
函数为链队列的头结点申请分配动态内存空间;由于在设置结构体时,已经设置了 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;
}
malloc
函数 为 结点 申请分配内存空间;x
填充到新的结点 node
;node
的指针域为空;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;
}
LinkQueue_Empty
来判断该链式队列是否为空 ,若为空则出队失败,返回 0 ;node
Q
指向链式队列的下一个数据元素,从而实现将 队首 的 后继结点 作为新的 队首 ;size
自减;思路同之前的其他结构读取首个数据元素相同;
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剩下的一个是现在应该出队的元素
解析源于 爱吃秋刀鱼