文章目录
- 一,队列
- 二,队列的实现方式
- 三,链式队列实现
-
- [1 Test.c](#1 Test.c)
- [2 Queue.h](#2 Queue.h)
- [3 Queue.c](#3 Queue.c)
- 四,链式队列解析
-
- [1 Test.c解析](#1 Test.c解析)
- [2 Queue.h解析](#2 Queue.h解析)
- [3 Queue.c解析](#3 Queue.c解析)
-
- [1 队列初始化](#1 队列初始化)
- [2 队列销毁](#2 队列销毁)
- [3 入队列](#3 入队列)
- [4 出队列](#4 出队列)
- [5 取队头节点的元素](#5 取队头节点的元素)
- [6 取队尾节点的元素](#6 取队尾节点的元素)
- [7 队列节点个数](#7 队列节点个数)
- [8 判断空队列](#8 判断空队列)
一,队列
- 定义:队列是操作受限的线性数据结构,严格遵循 先进先出(First In First Out, FIFO)的规则:只能在一端添加元素,另一端删除元素,添加元素的一端叫做队尾,添加元素也叫做入队列,删除元素的一端叫做队头,删除元素也叫做出队列
| 术语 |
含义 |
| 队头 |
允许删除元素的一端,对应最早进入队列的元素所在位置 |
| 队尾 |
允许添加元素的一端,对应最新进入队列的元素所在位置 |
| 入队 |
向队尾添加新元素的操作 |
| 出队 |
从队头移除元素的操作 |
二,队列的实现方式
| 队列类型 |
底层结构 |
入队/出队时间 |
辅助空间 |
容量特性 |
核心优势 |
核心劣势 |
典型场景 |
| 普通顺序队列 |
数组 |
入队O(1),平移式出队O(n) |
O(1) |
固定 |
逻辑最简单 |
存在假溢出问题,出队效率低 |
教学演示,无实际工程应用 |
| 循环队列 |
数组 |
均为O(1) |
O(1) |
固定 |
性能最高、空间利用率高、CPU缓存友好 |
容量固定,动态扩容成本高 |
嵌入式系统、操作系统内核、固定容量缓冲区 |
| 链式队列 |
单链表 |
均为O(1) |
O(n) |
动态 |
容量灵活,无需预分配空间 |
指针开销大、缓存命中率低、易产生内存碎片 |
元素数量波动极大的场景、教学入门 |
| 双端队列 |
分段数组 |
两端操作均为O(1) |
O(n) |
动态 |
功能最通用,性能均衡 |
底层实现复杂 |
高级语言标准库默认队列实现 |
| 优先队列 |
二叉堆 |
入队出队O(logn) |
O(1) |
动态 |
支持按优先级出队 |
不保证严格FIFO顺序 |
任务调度、TopK算法、最短路径算法 |
三,链式队列实现
1 Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
int main()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePush(&q, 5);
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
QueueDesTroy(&q);
return 0;
}
2 Queue.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
//队列初始化
void QueueInit(Queue* pq);
//队列销毁
void QueueDestroy(Queue* pq);
//入队列
void QueuePush(Queue* pq,QDataType x);
//出队列
void QueuePop(Queue* pq);
//取队头节点元素
QDataType QueueFront(Queue* pq);
//取队尾节点元素
QDataType QueueTail(Queue* pq);
//判断空队列(
bool QueueEmpty(Queue* pq);
//队列节点个数
int QueueSize(Queue* pq);
3 Queue.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
//队列销毁
void QueueDesTroy(Queue* pq)
{
assert(pq);
free(pq->phead);
Queue* pcur = pq->phead;
while (pcur)
{
Queue* next = pq->phead->next;
free(pcur);
pcur = next;
}
pq->size = 0;
}
//入队列
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc:");
return ;
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
//出队列
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->phead);
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
//取队头节点的元素
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
//取队尾节点的元素
QDataType QueueTail(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->ptail->val;
}
//队节点个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
//判断空队列
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;
}
四,链式队列解析
1 Test.c解析
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
int main()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
QueueDesTroy(&q);
return 0;
}
- 解析:需要用到Queue.h中的队列声明的操作函数和库函数,所以需要包含Queue.h,之前单链表创建的是一个指针变量用来存储头节点结构体的地址,然后操作函数的形参接收的就是这个指针变量的地址,形参变成了一个二级指针,就是为了可以修改因为函数操作而变化的指针变量中的头节点,也可以通过存有头节点指针变量的地址拿到头节点对链进行操作,但是我们链式队列的操作中,我们需要用到头节点的地址和尾节点的地址,所以我们把链式队列的头节点和尾节点一起封装在一个结构体中,再把链式队列的节点个数size也封装在了这个结构体中,所以我们需要使用这个队列结构体类型创建一个结构体变量,将这个结构体变量的地址传入操作函数中,这样队列操作函数就能通过队列结构体地址拿到队列的头节点和尾节点以及队列的节点个数,创建好队列结构体以后,我们还需要进行队列初始化操作,初始化队列以后,就可以进行队列操作了
- 解析:while循环打印队列节点中数据的操作,判空作为while循环的判断条件,打印一次队列队头数据就出队一次,这样就能循环打印队头,最后队列全部节点打印完成的时候队列为NULL,停止打印,最后销毁队列
2 Queue.h解析
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
/队列初始化
void QueueInit(Queue* pq);
//队列销毁
void QueueDestroy(Queue* pq);
//入队列
void QueuePush(Queue* pq,QDataType x);
//出队列
void QueuePop(Queue* pq);
//取队头节点元素
QDataType QueueFront(Queue* pq);
//取队尾节点元素
QDataType QueueTail(Queue* pq);
//判断空队列(
bool QueueEmpty(Queue* pq);
//队列节点个数
int QueueSize(Queue* pq);
- 解析:包含需要用到的库函数和声明所以队列的操作函数
typedef int QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
- 解析:QNode是链式队列中的节点,在一个结构体内创建val成员和next成员,val用来存放数据,next存放节点地址,数据类型重命名为QDataType,Queue就是之前说的队列结构体,里面封装着链式队列队头phead和队尾节点的地址ptail,以及队列节点个数size
3 Queue.c解析
1 队列初始化
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
- 解析:断言pq,把phead 和 ptail 置为 NULL,把size置为0
2 队列销毁
//队列销毁
void QueueDesTroy(Queue* pq)
{
assert(pq);
Queue* pcur = pq->phead;
while (pcur)
{
Queue* next = pq->phead->next;
free(pcur);
pcur = next;
}
pq->size = 0;
}
- 解析:断言pq,链式队列需要循环释放每一个malloc出来的节点,我们把头节点保存在pcur中,pcur作为while循环的判断条件遍历链表,先把当前节点的下一个节点保存在next指针变量中,用free释放当前节点以后将next赋给pcur,让pcur走到下一个节点,循环释放节点,最后释放完所有队列节点以后把size置为0
3 入队列
//入队列
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc:");
return ;
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
- 解析:断言pq,在链式队列中入队列就是创建一个个节点,并且把节点插入到队尾,直接写单链表的时候创建节点的操作是封装在一个创建节点函数中的,因为单链表中有不同位置的插入,所有在不同位置的插入中都需要创建节点,就把创建节点封装成了一个函数,只要需要插入的地方就调用这个创建节点的函数,插入函数只需要调整创建好的节点插入的位置即可,但是链式队列中插入操作只出现在队尾,只出现在入队列的操作中,所以不需要把创建节点单独封装成一个函数,只需要在入队列操作中创建节点即可,所以malloc创建节点,把节点的next置为NULL,节点的val置为需要插入的节点的数据,在单链表中就是尾插,入队列时,如果插入的节点时队列仅仅被初始化,插入的节点时唯一一个节点,那么我们就要将链式队列的头节点和尾节点同时指向这个唯一的节点,因为只有一个节点,所以这个节点既做头节点也做尾节点,如果链式队列中以及有节点了,那入队列就直接在链式结构中进行尾插即可,判断链式队列中是否只有一个节点的方法是,如果队列中只有一个节点的话,头节点的next为NULL,最后入队列操作完成以后要让size++
4 出队列
//出队列
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->phead);
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
- 解析:断言pq和队列,如果队列为NULL 的话,无法进行出队列操作,出队列操作分两种情况,一种是只有一个节点的队列,另一种是有多个节点出队列的情况,因为我们的队列结构体有两个指针,一个是头节点的指针,一个是尾节点的指针,如果是多个节点的情况下出队列的操作只会影响头节点的指针,不会影响尾节点的指针,但是如果链式队列中只有一个节点的情况下,头节点的指针和尾节点的指针同时指向这个唯一的节点,出队列的话,会影响头节点指针和尾节点指针,因为这个节点既是头节点也是尾节点,如果我们还是按照多个节点一样的方式去进行出队列操作的话,只改变头节点指针,那尾节点指针指向的位置就是刚刚出队列的节点,也就是刚刚被释放的节点,这时候的头节点指针以及被改变了,指向了NULL,但是尾节点指针依然指向原来的位置,这时候的尾节点指针指向的空间是被释放了的空间,尾节点指针就是典型的野指针,所以我们要对队列中只有一个节点情况单独讨论,如果队列中只有一个节点,那在队头出队列以后,整个队列都没有节点了,这时候要把队列的头节点指针和尾节点指针都置为NULL,因为队列中仅有一个节点的时候出队列会不仅仅会影响头节点指针,还会影响尾节点指针,所以两个节点指针都要置为NULL,剩下的情况就是队列中不止一个节点,那最后需要改变的只有头节点指针,先把头节点的下一个节点存起来,然后释放头节点完成出队,再让下一个节点称为新的头节点。最后无论哪一种情况出队以后都要让size--
5 取队头节点的元素
//取队头节点的元素
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
- 解析:断言pq和phead,取头节点元素就要保证链式队列不为NULL,所以断言头节点,队列结构体中存了头节点指针,通过指针找到头节点的元素返回即可
6 取队尾节点的元素
//取队尾节点的元素
QDataType QueueTail(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->ptail->val;
}
- 解析:断言pq和phead,取尾节点元素就要保证链式队列不为NULL,所以断言头节点,队列结构体中存了指针,尾节点指针找到尾节点的元素返回即可
7 队列节点个数
//对了节点个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
- 解析:断言pq,队列结构体中已经保存了队列节点个数在成员size里面了,当进行入队操作的时候size++,当出队操作的时候size--,所以链式队列结构体中成员size的大小一直匹配着链式队列中节点的个数,所以直接放回size就可以得到队列的节点个数
8 判断空队列
//判断空队列
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;
}
- 解析:返回判断表达式pq->phead == NULL 的结果,如果phead == NULL成立说明队列为NULL,表达式的结果为true,如果phead == NULL不成立说明队列不为NULL,表达式结果为flase,返回表格式的结果就可以判断空队列,函数返回值类型为bool类型