什么是队列
队列,和栈一样,也是一种对数据的"存"和"取"有严格要求的线性存储结构。
与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出,如图所示:
通常,称进数据的一端为 "队尾",出数据的一端为 "队头",数据元素进队列的过程称为 "入队",出队列的过程称为 "出队"。
不仅如此,队列中数据的进出要遵循 "先进先出" 的原则,即最先进队列的数据元素,同样要最先出队列。拿图 中的队列来说,从数据在队列中的存储状态可以分析出,元素 1 最先进队,其次是元素 2,最后是元素 3。此时如果将元素 3 出队,根据队列 "先进先出" 的特点,元素 1 要先出队列,元素 2 再出队列,最后才轮到元素 3 出队列。
栈和队列不要混淆,栈结构是一端封口,特点是"先进后出";而队列的两端全是开口,特点是"先进先出"。
因此,数据从表的一端进,从另一端出,且遵循 "先进先出" 原则的线性存储结构就是队列。
队列存储结构的实现有以下两种方式:
- 顺序队列:在顺序表的基础上实现的队列结构;
- 链队列:在链表的基础上实现的队列结构;
两者的区别仅是顺序表和链表的区别,即在实际的物理空间中,数据集中存储的队列是顺序队列,分散存储的队列是链队列。
实际生活中,队列的应用随处可见,比如排队买 XXX、医院的挂号系统等,采用的都是队列的结构。拿排队买票来说,所有的人排成一队,先到者排的就靠前,后到者只能从队尾排队等待,队中的每个人都必须等到自己前面的所有人全部买票成功并从队头出队后,才轮到自己买票。这就是典型的队列结构。
明白了什么是队列,接下来开始系统地学习顺序队列和链队列。
顺序存储(数组)实现队列
由于顺序队列的底层使用的是数组,因此需预先申请一块足够大的内存空间初始化顺序队列。除此之外,为了满足顺序队列中数据从队尾进,队头出且先进先出的要求,我们还需要定义两个指针(top 和 rear)分别用于指向顺序队列中的队头元素和队尾元素,如图 1 所示:
由于顺序队列初始状态没有存储任何元素,因此 top 指针和 rear 指针重合,且由于顺序队列底层实现靠的是数组,因此 top 和 rear 实际上是两个变量,它的值分别是队头元素和队尾元素所在数组位置的下标。
在图 1 的基础上,当有数据元素进队列时,对应的实现操作是将其存储在指针 rear 指向的数组位置,然后 rear+1;当需要队头元素出队时,仅需做 top+1 操作。
例如,在图 1 基础上将 {1,2,3,4} 用顺序队列存储的实现操作如图 2 所示:
在图 2 基础上,顺序队列中数据出队列的实现过程如图 3 所示:
此方法存在的问题
先来分析以下图 2b) 和图 3b)。图 2b) 是所有数据进队成功的示意图,而图 3b) 是所有数据全部出队后的示意图。通过对比两张图,你会发现,指针 top 和 rear 重合位置指向了 a[4] 而不再是 a[0]。也就是说,整个顺序队列在数据不断地进队出队过程中,在顺序表中的位置不断后移。
顺序队列整体后移造成的影响是:
- 顺序队列之前的数组存储空间将无法再被使用,造成了空间浪费;
- 如果顺序表申请的空间不足够大,则直接造成程序中数组 a 溢出,产生溢出错误;
既然明白了上面这种方法的弊端,那么我们可以试着在它的基础上对其改良。
为了解决以上两个问题,可以使用巧妙的方法将顺序表打造成一个环状表,如图 4 所示:
图 4 只是一个想象图,在真正的实现时,没必要真创建这样一种结构,只需要对其进行一点小小的处理,指针到了顺序表最后一位时加一时指向首位。
使用此方法需要注意的是,顺序队列在判断数组是否已满时,出现下面情况:
- 当队列为空时,队列的头指针等于队列的尾指针;
- 当数组满员时,队列的头指针等于队列的尾指针;
顺序队列的存储状态不同,但是判断条件相同。为了对其进行区分,最简单的解决办法是:牺牲掉数组中的一个存储空间,判断数组满员的条件是:尾指针的下一个位置和头指针相遇,就说明数组满了。
代码如下:
定义队列结构
#define MAX_SIZE 10
typedef int ElemType;
typedef struct{
ElemType data[MAX_SIZE];//存储内容
int front;//头指针
int rear; //尾指针
}HjQueue;
1、初始化队列
void initQueue(HjQueue *queue){
queue->front = 0;
queue->rear = 0;
}
2、判断队列是否为空
Status isEmptyQueue(HjQueue queue){
return queue.front == queue.rear;
}
3、判断队列是否已满
Status isFullQueue(HjQueue queue){
return (queue.rear + 1) % MAX_SIZE == queue.front;
}
4、清空队列
void clearQueue(HjQueue *queue){
queue->front = 0;
queue->rear = 0;
}
5、返回队列的长度
int getQueueLength(HjQueue queue){
return (queue.rear + MAX_SIZE - queue.front) % MAX_SIZE;
}
6、获取队列的头元素
Status getQueueHeadData(HjQueue queue, ElemType *data){
if (queue.front == queue.rear) {
return ERROR;
}
if (queue.front < MAX_SIZE) {
*data = queue.data[queue.front];
return OK;
}
return ERROR;
}
7、插入元素到队列中
Status insertDataIntoQueue(HjQueue *queue, ElemType data){
//如果队列已满,则不能插入元素
int newRear = (queue->rear + 1) % MAX_SIZE;
if (newRear == queue->front) {
return ERROR;
}
queue->data[queue->rear] = data;
queue->rear = newRear;
return OK;
}
8、从队列中删除元素
Status deleteDataFromQueue(HjQueue *queue, ElemType *data){
//如果队列已空,则不能删除
if (queue->front == queue->rear) {
return ERROR;
}
//删除元素只需要头指针动就可以了
*data = queue->data[queue->front];
queue->front = (queue->front + 1) % MAX_SIZE;
return OK;
}
9、遍历打印队列
void printfQueue(HjQueue queue){
printf("遍历队列:");
int i = queue.front;
while (i != queue.rear) {
printf("%d ",queue.data[I]);
i = (i + 1) % MAX_SIZE;
}
printf("\n");
}
其它辅助代码
#include "stdlib.h"
#define OK 1
#define ERROR 0
typedef int Status;
int main(int argc, const char * argv[]) {
printf("--顺序队列操作实现--\n");
HjQueue queue;
ElemType data;
printf("初始化队列\n");
initQueue(&queue);
printf("队列是否为空?(1:空 0:否) %d\n",isEmptyQueue(queue));
//入队
printf("操作入队:\n");
for (int i = 1; i <= 10; i++) {
insertDataIntoQueue(&queue, i);
}
printfQueue(queue);
printf("队列是否为空?(1:空 0:否) %d\n",isEmptyQueue(queue));
printf("队列是否已满?(1:满 0:否) %d\n",isFullQueue(queue));
printf("队列长度为: %d\n",getQueueLength(queue));
//出队
printf("操作出队:\n");
deleteDataFromQueue(&queue,&data);
printf("出队的元素: %d\n",data);
printfQueue(queue);
//获取队头
Status status = getQueueHeadData(queue, &data);
if (status) {
printf("现在队头元素为: %d\n",data);
}
//清空队列
printf("清空队列后");
clearQueue(&queue);
printf("队列是否为空?(1:空 0:否) %d\n",isEmptyQueue(queue));
return 0;
}
输出结果
链式存储(链表)实现队列
链式队列的实现思想同顺序队列类似,只需创建两个指针(命名为 top 和 rear)分别指向链表中队列的队头元素和队尾元素,如图 1 所示:
图 1 所示为链式队列的初始状态,此时队列中没有存储任何数据元素,因此 top 和 rear 指针都同时指向头节点。我写的链式队列有头节点,因为这样更简单。
链式队列中,当有新的数据元素入队,只需进行以下 3 步操作:
- 将该数据元素用节点包裹,例如新节点名称为 node;
- 与 rear 指针指向的节点建立逻辑关系,即执行 rear->next = node;
- 最后移动 rear 指针指向该新节点,即 rear = node;
由此,新节点就入队成功了。
例如,在图 1 的基础上,我们依次将 {1,2,3} 依次入队,各个数据元素入队的过程如图 2 所示:
当链式队列中,有数据元素需要出队时,按照 "先进先出" 的原则,只需将存储该数据的节点以及它之前入队的元素节点按照原则依次出队即可。这里,我们先学习如何将队头元素出队。链式队列中队头元素出队,需要做以下 3 步操作:
- 通过 top 指针直接找到队头节点,创建一个新指针 p 指向此即将出队的节点;
- 将 p 节点(即要出队的队头节点)从链表中摘除;
- 释放节点 p,回收其所占的内存空间;
例如,在图 2b) 的基础上,我们将元素 1 和 2 出队,则操作过程如图 3 所示:
实现代码如下:
定义队列结构
typedef int ElemType;
typedef struct HjLinkNode{
ElemType data;//存储内容
struct HjLinkNode *next;
}LinkNode, *QueueNode;
typedef struct HjQueue{
QueueNode front;//队头指针
QueueNode rear;//队尾指针
}HjQueue;
1、初始化队列
Status initQueue(HjQueue *queue){
queue->front = malloc(sizeof(LinkNode));
if (!queue->front) {
return ERROR;
}
//数据初始化
queue->front->next = NULL;
queue->rear = queue->front;
return ERROR;
}
2、判断队列是否为空
Status isEmptyQueue(HjQueue queue){
return queue.front->next == NULL;
}
3、销毁队列
void destoryQueue(HjQueue *queue){
QueueNode node = queue->front;
QueueNode temp;
while (node) {
temp = node;
node = node->next;
free(temp);
}
queue->front = queue->rear = NULL;
}
4、清空队列
void clearQueue(HjQueue *queue){
queue->rear = queue->front;
QueueNode node = queue->front->next;
QueueNode temp;
while (node) {
temp = node;
node = node->next;
free(temp);
}
}
5、返回队列的长度
int getQueueLength(HjQueue queue){
QueueNode node = queue.front->next;
int count = 0;
while (node) {
count ++;
node = node->next;
}
return count;
}
6、获取队列的头元素
Status getQueueHeadData(HjQueue queue, ElemType *data){
//判断是否空队列
QueueNode node = queue.front->next;
if (!node) {
return ERROR;
}
*data = node->data;
return OK;
}
7、插入元素到队列中
Status insertDataIntoQueue(HjQueue *queue, ElemType data){
if (!queue->rear) {
return ERROR;
}
QueueNode node = malloc(sizeof(LinkNode));
if (!node) {
return ERROR;
}
node->data = data;
node->next = NULL;
queue->rear->next = node;
queue->rear = node;
return OK;
}
8、从队列中删除元素
Status deleteDataFromQueue(HjQueue *queue, ElemType *data){
//如果队列已空,则不能删除
QueueNode node = queue->front->next;
if (!node) {
return ERROR;
}
queue->front->next = node->next;
if (queue->rear == node) {
queue->rear = queue->front;
}
*data = node->data;
free(node);
return OK;
}
9、遍历打印队列
void printfQueue(HjQueue queue){
printf("遍历队列:");
QueueNode node = queue.front->next;
while (node) {
printf("%d ",node->data);
node = node->next;
}
printf("\n");
}
其它辅助代码
#include "stdlib.h"
#define OK 1
#define ERROR 0
typedef int Status;
int main(int argc, const char * argv[]) {
printf("--链式队列操作实现--\n");
HjQueue queue;
ElemType data;
printf("初始化队列\n");
initQueue(&queue);
printf("队列是否为空?(1:空 0:否) %d\n",isEmptyQueue(queue));
//入队
printf("操作入队:\n");
for (int i = 1; i <= 10; i++) {
insertDataIntoQueue(&queue, i);
}
printfQueue(queue);
printf("队列是否为空?(1:空 0:否) %d\n",isEmptyQueue(queue));
printf("队列长度为: %d\n",getQueueLength(queue));
//出队
printf("操作出队:\n");
deleteDataFromQueue(&queue,&data);
printf("出队的元素: %d\n",data);
printfQueue(queue);
//获取队头
Status status = getQueueHeadData(queue, &data);
if (status) {
printf("现在队头元素为: %d\n",data);
}
//清空队列
printf("清空队列后");
clearQueue(&queue);
printf("队列是否为空?(1:空 0:否) %d\n",isEmptyQueue(queue));
return 0;
}
输出结果
如有不对的地方,请指正,谢谢您的阅读~