目录
队列
队列的基本概念
队列的定义:
- 队列(Queue)是一种受限制的线性表。
- 特点是:先进先出(FIFO, First In First Out) 。
- 元素只能在 队尾(rear) 插入。
- 元素只能在 队头(front) 删除。
可以想象一下:就像你排队买票,先到的人先买,后到的人只能排在后面,不能插队。
队列的基本操作:
- 初始化队列:创建一个空队列。
- 入队(EnQueue):新元素加入到队尾。
- 出队(DeQueue):删除队头的元素,并返回它。
- 取队头元素(GetFront):只读取队头的元素,但不删除。
- 判空(IsEmpty):判断队列里是否还有元素。
- 判满(IsFull)(针对顺序存储的队列):判断队列是否已满。
队列的分类:
- 顺序队列 :用数组实现,队头队尾用下标表示。
- 缺点:出队时会产生"假溢出"现象(队头不断右移,空间浪费)。
- 链式队列:用链表实现,动态分配内存,不存在"溢出",更灵活。
- 循环队列 :对顺序队列的改进,把数组头尾"连成环",解决假溢出问题。
- 公式:
(rear + 1) % MaxSize == front
表示队满。
- 公式:
**图示理解:**假设队列最多能存放 5 个元素:
csharp
初始:front=0, rear=0, 队列空
[ ] [ ] [ ] [ ] [ ]
入队 A:rear=1
[A] [ ] [ ] [ ] [ ]
入队 B:rear=2
[A] [B] [ ] [ ] [ ]
出队(删除 A):front=1
[ ] [B] [ ] [ ] [ ]
入队 C:rear=3
[ ] [B] [C] [ ] [ ]
依次进行......
为什么顺序存储一定会"溢出"?
顺序存储结构 (顺序表、顺序队列、顺序栈...)的特点是:
👉 用一块 连续的内存空间 来保存数据。
定义时必然有一个
MaxSize
(不管是静态数组还是动态malloc
出来的数组)。插入/入队/入栈超过
MaxSize
时,就会发生 溢出。即使用
realloc
扩容,本质上还是"再申请更大的一块连续空间 → 搬数据",最终也受系统内存限制。为什么链式存储不会"假溢出"?
链式存储结构 (链表、链式队列、链式栈...)的特点是:
👉 每个结点通过 指针域 连接到下一个结点。
不需要一块连续的内存,理论上只要能
malloc
出一个新结点,就能继续插入。所以它没有固定的
MaxSize
,也不会产生"假溢出"问题。系统层面的最终约束
不管是 顺序存储 还是 链式存储:
如果内存耗尽(比如系统没有空闲空间再给你
malloc
结点 / 数组),都会失败。区别在于:
顺序存储 :即使内存够,但你的
MaxSize
限死了容量 → 提前溢出。链式存储 :只要内存够,就能继续扩展,没有人为的
MaxSize
。总结一句话 ✅
顺序存储类(顺序表/队列/栈) :因为有
MaxSize
,会溢出;链式存储类(链表/链式队列/链式栈):没有固定上限,只要内存够就能继续;
但 最终大家都受系统内存限制,内存不足时都会"溢出"。
顺序队列
顺序队列是用 数组 来实现的队列。
特点:
- 使用数组连续存储元素
- 有两个指标:
front
:队头位置rear
:队尾下一个空位位置
注意 :这里 rear
指向的是 下一个可插入元素的位置,不是队尾元素的位置。
- 判断队空和队满:
- 队空:
front == rear
- 队满(顺序队列固定大小):
rear == maxsize
- 队空:
- 固定大小顺序队列可能会出现"假溢出":数组还有空位,但由于前面有元素出队,rear到达数组末尾也会认为队满。
顺序队列的定义与初始化
顺序队列的定义:
c
#define MAXSIZE 100
typedef struct {
int data[MAXSIZE]; // 存储元素
int front; // 队头
int rear; // 队尾下一个位置
} SqQueue;
这部分定义了一个结构体SqQueue
(通常表示 "顺序队列",Sequential Queue),用于描述队列的存储结构,包含 3 个成员:
int data[MAXSIZE]
:一个 int 类型的数组,用于实际存储队列中的元素,数组大小由MAXSIZE
指定(即最多可存储 100 个 int 元素)。int front
:表示 "队头指针",记录队列中第一个元素的位置(索引)。通过front
可以快速获取队头元素。int rear
:表示 "队尾指针的下一个位置",记录队列中最后一个元素的下一个空闲位置(索引)。当有新元素入队时,会将元素存放在rear
指向的位置,再更新rear
。
顺序队列的初始化:
c
void InitQueue(SqQueue *Q) {
Q->front = 0;
Q->rear = 0;
}
Q->front = 0;
:将队列的队头指针front
初始化为 0(指向数组的起始索引)。Q->rear = 0;
:将队列的队尾指针rear
也初始化为 0(同样指向数组的起始索引)。
顺序队列的入队/出队
入队操作:
c
int EnQueue(SqQueue *Q, int e) {
if(Q->rear == MAXSIZE) // 队满
return 0;
Q->data[Q->rear] = e;
Q->rear++;
return 1;
}
- 函数定义 :
int EnQueue(SqQueue *Q, int e)
- 返回值类型为
int
:通常用1
表示入队成功,0
表示入队失败(队列已满)。 - 参数
SqQueue *Q
:指向队列结构体的指针,用于操作目标队列(通过指针修改队列的实际数据)。 - 参数
int e
:要插入队列的元素(int 类型)。
- 返回值类型为
- 核心逻辑 :
- 第一步(判断队列是否已满):
if(Q->rear == MAXSIZE) return 0;
检查队尾指针rear
是否等于最大容量MAXSIZE
。由于rear
表示 "队尾元素的下一个位置",当rear
达到MAXSIZE
时,说明数组data
已被填满(没有空闲位置),此时队列已满,无法插入新元素,返回0
表示失败。 - 第二步(插入元素):
Q->data[Q->rear] = e;
若队列未满,将元素e
存入data
数组中rear
指向的位置(即当前队尾的下一个空闲位置)。 - 第三步(更新队尾指针):
Q->rear++;
插入元素后,将rear
指针向后移动一位(指向新的队尾下一个位置),为下一次入队做准备。 - 第四步(返回成功标识):
return 1;
入队操作完成,返回1
表示成功。
- 第一步(判断队列是否已满):
出队操作:
c
int DeQueue(SqQueue *Q, int *e) {
if(Q->front == Q->rear) // 队空
return 0;
*e = Q->data[Q->front];
Q->front++;
return 1;
}
- 函数定义 :
int DeQueue(SqQueue *Q, int *e)
- 返回值类型为
int
:用1
表示出队成功,0
表示出队失败(队列已空)。 - 参数
SqQueue *Q
:指向队列结构体的指针,用于操作目标队列。 - 参数
int *e
:一个 int 类型的指针,用于 "传出" 被删除的队头元素(通过指针可以将函数内部的值传递到函数外部)。
- 返回值类型为
- 核心逻辑 :
- 第一步(判断队列是否为空):
if(Q->front == Q->rear) return 0;
检查队头指针front
是否等于队尾指针rear
。根据队列约定,当两者相等时表示队列为空(没有元素可删除),此时返回0
表示出队失败。 - 第二步(获取并传出队头元素):
*e = Q->data[Q->front];
若队列非空,将front
指向的队头元素(data[front]
)赋值给指针e
指向的变量(即通过e
将队头元素 "带出去" 给函数调用者)。 - 第三步(更新队头指针):
Q->front++;
删除队头元素后,将front
指针向后移动一位(指向新的队头元素),完成出队操作。 - 第四步(返回成功标识):
return 1;
出队操作完成,返回1
表示成功。
- 第一步(判断队列是否为空):
顺序队列的判空/判满
判空操作:
c
// 判断队空
int QueueEmpty(SqQueue *Q) {
return Q->front == Q->rear;
}
- 函数定义 :
int QueueEmpty(SqQueue *Q)
- 返回值类型为
int
:在 C 语言中,通常用1
表示 "真"(队列为空),0
表示 "假"(队列非空)。 - 参数
SqQueue *Q
:指向队列结构体的指针,用于访问队列的front
和rear
指针。
- 返回值类型为
- 核心逻辑 :
函数体只有一行:return Q->front == Q->rear;
- 表达式
Q->front == Q->rear
用于判断 "队头指针" 和 "队尾指针" 是否指向同一个位置。 - 根据队列的约定(结合之前的初始化、入队、出队逻辑):当
front
和rear
相等时,队列中没有任何元素(为空);当两者不相等时,队列中至少有一个元素(非空)。 - 该表达式的结果是一个 "布尔值":若相等则为
1
(真),表示队空;若不相等则为0
(假),表示队列非空。
- 表达式
判满操作:
c
// 判断队满
int QueueFull(SqQueue *Q) {
return Q->rear == MAXSIZE;
}
- 函数定义 :
int QueueFull(SqQueue *Q)
- 返回值类型为
int
:在 C 语言中,通常用1
表示 "真"(队列已满),0
表示 "假"(队列未满)。 - 参数
SqQueue *Q
:指向队列结构体的指针,用于访问队列的rear
指针和最大容量MAXSIZE
。
- 返回值类型为
- 核心逻辑 :
函数体只有一行:return Q->rear == MAXSIZE;
- 表达式
Q->rear == MAXSIZE
用于判断 "队尾指针的下一个位置" 是否已达到队列的最大容量(MAXSIZE
)。 - 根据之前的入队逻辑(
EnQueue
函数),rear
始终指向队尾元素的下一个空闲位置,且每次入队后rear
会递增(rear++
)。因此,当rear
等于MAXSIZE
时,意味着数组data
的所有位置(0
到MAXSIZE-1
)都已被元素占用,没有空闲空间可供新元素插入 ------ 即队列已满。 - 该表达式的结果为 "布尔值":若相等则返回
1
(真,表示队满);若不相等则返回0
(假,表示队列未满)。
- 表达式
获取队头元素
获取队头操作:
c
// 获取队头元素
int GetHead(SqQueue *Q, int *e) {
if (QueueEmpty(Q)) return 0;
*e = Q->data[Q->front];
return 1;
}
- 函数定义 :
int GetHead(SqQueue *Q, int *e)
- 返回值类型为
int
:用1
表示获取成功(队列非空),0
表示获取失败(队列为空)。 - 参数
SqQueue *Q
:指向队列结构体的指针,用于访问队列的元素和指针。 - 参数
int *e
:int 类型的指针,用于 "传出" 获取到的队头元素(通过指针将队头元素的值传递给函数外部)。
- 返回值类型为
- 核心逻辑 :
- 第一步(判断队列是否为空):
if (QueueEmpty(Q)) return 0;
调用之前定义的QueueEmpty
函数判断队列是否为空。若队空(front == rear
),则没有队头元素可获取,返回0
表示失败。 - 第二步(获取队头元素并传出):
*e = Q->data[Q->front];
若队列非空,直接读取front
指针指向的元素(data[front]
,即队头元素),并通过指针e
将其值传递给外部变量。 - 第三步(返回成功标识):
return 1;
成功获取队头元素后,返回1
表示操作成功。
- 第一步(判断队列是否为空):
队列状态:
- 队空:
front == rear
- 队满(固定大小顺序队列):
rear == MAXSIZE
⚠️ 注意:顺序队列在多次入队出队后,前面出队留下的空间不会自动利用,所以容易"假溢出"。
动态顺序队列:
如果用 malloc
动态分配数组,就可以避免固定大小队列的溢出问题:
- 当队尾到达数组末尾但队头有空位时,可以 搬移元素 或 扩容数组
- 使用循环队列(Circular Queue)可以更高效利用空间
循环队列
顺序队列有个大问题就是会出现 "假溢出" ------明明数组前面有空位置,但 rear
到了末尾就不能再入队了。
👉 为了解决这个问题,就出现了 循环队列(Circular Queue)。
循环队列的概念
循环队列就是把顺序队列的存储空间 看成一个环形 ,利用取模运算 (mod)
让 front
和 rear
在数组末尾能"绕回"开头,循环使用存储空间。
指针变化:
- 队头指针
front
:指向队头元素 - 队尾指针
rear
:指向 下一个要插入的位置

虽然循环队列可以看成环形,但实际上仍然是一个顺序结构
循环队列的状态判断
为了避免 front == rear
同时表示队满和队空的问题,通常采用两种方法之一:
- 少用一个存储单元 (最常用)
- 队空:
front == rear
- 队满:
(rear + 1) % MaxSize == front
这样最多只能存MaxSize - 1
个元素。
- 队空:
- 增加一个计数器
- 用一个变量记录队列中元素的数量
- 队空:
size == 0
- 队满:
size == MaxSize
循环队列的定义
c
#define MAXQSIZE 100 // 最大队列长度
typedef int QElemType; // 这里可以根据需要定义成int/char/struct等
typedef struct {
QElemType *base; // 存储空间的基址(动态分配)
int front; // 队头指针,指向队头元素
int rear; // 队尾指针,指向队尾元素的下一个位置
} SqQueue;
QElemType *base
:
一个指向QElemType
类型的指针,用于指向队列元素的存储空间(通常是动态分配的数组)。"基址" 表示这个指针指向整个存储空间的起始位置。int front
:
队头指针(此处用整数表示数组索引),指向队列中第一个元素的位置。int rear
:
队尾指针(同样用整数表示数组索引),指向队列中最后一个元素的下一个位置(即下一个入队元素将要存放的位置)。
循环队列的初始化
c
// 初始化队列
int InitQueue(SqQueue *Q) {
Q->base = (QElemType *)malloc(MAXQSIZE * sizeof(QElemType));
if (!Q->base) exit(0); // 分配失败
Q->front = Q->rear = 0;
return 1;
}
函数定义:int InitQueue(SqQueue *Q)
- 函数名
InitQueue
:表示 "初始化队列"(Initialize Queue)。 - 参数
SqQueue *Q
:传入队列结构体的指针,用于操作具体的队列实例(通过指针修改原队列的成员)。 - 返回值
int
:通常用1
表示初始化成功,0
表示失败(此处成功返回1
,失败直接退出程序)。
函数内部逻辑:
-
分配存储空间
cQ->base = (QElemType *)malloc(MAXQSIZE * sizeof(QElemType));
malloc
函数动态分配一块内存,大小为MAXQSIZE * sizeof(QElemType)
:MAXQSIZE
是队列最大长度(之前定义的 100);sizeof(QElemType)
是单个元素的字节大小(由QElemType
类型决定,如int
占 4 字节)。
- 分配的内存用于存储队列元素,将其首地址强制转换为
QElemType*
类型后,赋值给队列的base
成员(即指向存储空间的基址)。
-
检查内存分配是否成功
cif (!Q->base) exit(0); // 分配失败
- 如果
malloc
分配内存失败,会返回NULL
,此时Q->base
为NULL
(!Q->base
为真)。 - 遇到这种情况,调用
exit(0)
直接终止程序(实际开发中可能更优雅地返回错误码,此处简化处理)。
- 如果
-
初始化队头和队尾指针
cQ->front = Q->rear = 0;
- 初始化时队列是空的,因此将
front
(队头指针)和rear
(队尾指针)都设置为0
(指向数组的起始位置)。 - 在顺序队列中,
front == rear
是 "队列空" 的判断条件。
- 初始化时队列是空的,因此将
-
返回成功标志
creturn 1;
- 所有初始化操作完成后,返回
1
表示队列初始化成功。
- 所有初始化操作完成后,返回
循环队列的判空操作
c
// 队空
int QueueEmpty(SqQueue Q) {
return Q.front == Q.rear;
}
函数定义:int QueueEmpty(SqQueue Q)
- 函数名
QueueEmpty
:表示 "队列是否为空"(Queue Empty)。 - 参数
SqQueue Q
:传入队列结构体的值(而非指针),因为判断队列是否为空只需要读取队列的状态,不需要修改队列,所以传值即可。 - 返回值
int
:在 C 语言中,通常用1
(非 0 值)表示 "真"(队列空),0
表示 "假"(队列非空)。
函数逻辑:return Q.front == Q.rear;
这行代码的核心是比较队头指针front
和队尾指针rear
是否相等:
- 当
Q.front == Q.rear
时,表达式结果为1
(真),函数返回1
,表示队列是空的; - 当
Q.front != Q.rear
时,表达式结果为0
(假),函数返回0
,表示队列中存在元素(非空)。
为什么
front == rear
能判断队空?这与队列的设计逻辑一致:
- 初始化队列时(
InitQueue
函数),front
和rear
被同时设置为0
(Q->front = Q->rear = 0
),此时队列为空;- 当队列中的元素全部出队后,
front
会逐渐移动到与rear
相同的位置,此时队列再次为空。因此,
front
与rear
相等是 "队列空" 的标志性条件。
循环队列的判满操作
c
// 队满
int QueueFull(SqQueue Q) {
return (Q.rear + 1) % MAXQSIZE == Q.front;
}
函数定义:int QueueFull(SqQueue Q)
- 函数名
QueueFull
:表示 "队列是否已满"(Queue Full)。 - 参数
SqQueue Q
:传入队列结构体的值(无需修改队列,仅读取状态)。 - 返回值
int
:用1
表示 "队列已满"(真),0
表示 "队列未满"(假)。
核心判断逻辑:return (Q.rear + 1) % MAXQSIZE == Q.front;
这行代码的设计是为了避免普通顺序队列的 "假溢出" 问题,我们需要结合 "循环队列" 的思想理解:
- 为什么不能用
Q.rear == MAXQSIZE
判断队满?
普通顺序队列中,如果简单用 "队尾指针rear
达到最大长度MAXQSIZE
" 判断队满,会出现假溢出 :- 例如:队列初始为空(
front=0, rear=0
),依次入队元素后rear
逐渐增大到MAXQSIZE
(此时看似满了),但如果此时有元素出队,front
会向后移动(比如front=2
),队列前面会出现空位(索引 0、1),但rear
已达最大值,无法再入队,导致空间浪费。
- 例如:队列初始为空(
- 循环队列的解决方案:
将队列的存储空间视为一个环形结构 (通过取模运算实现 "绕回"),此时队满的判断条件设计为:(rear + 1) % MAXQSIZE == front
含义是:当队尾指针rear
的下一个位置 (加 1 后),通过取模MAXQSIZE
绕回环形空间后,与队头指针front
重合时,说明队列已满。 - 取模运算的作用:
% MAXQSIZE
确保指针在达到数组边界(MAXQSIZE-1
)后,能 "绕回" 到起始位置(0),形成环形。例如:- 若
MAXQSIZE=100
,当rear=99
(最后一个索引)时,rear+1=100
,100 % 100 = 0
,此时若front=0
,则满足队满条件。
- 若
- 为什么要留一个空位?
这种判断方式会故意保留一个空闲位置 (即rear
指向的位置始终为空),目的是区分 "队满" 和 "队空":- 队空条件:
front == rear
(两者指向同一位置,且该位置无元素); - 队满条件:
(rear+1) % MAXQSIZE == front
(rear
的下一个位置是front
,中间无空闲空间)。
- 队空条件:
如果不留空位,当队列满时也会出现 front == rear
,就无法区分是 "空" 还是 "满" 了。
循环队列的入队操作
c
int EnQueue(SqQueue *Q, QElemType e) {
if (QueueFull(*Q)) return 0; // 队满
Q->base[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXQSIZE;
return 1;
}
函数定义:int EnQueue(SqQueue *Q, QElemType e)
- 函数名
EnQueue
:表示 "入队"(Enqueue,即元素进入队列)。 - 参数
SqQueue *Q
:队列结构体的指针,用于修改队列的状态(如队尾指针rear
和存储元素)。 - 参数
QElemType e
:要插入队列的元素(类型由QElemType
定义,如int
)。 - 返回值
int
:1
表示入队成功,0
表示入队失败(队列已满时)。
函数内部逻辑:
-
判断队列是否已满
cif (QueueFull(*Q)) return 0; // 队满
- 调用之前定义的
QueueFull
函数判断队列是否已满(传入*Q
是因为QueueFull
参数是结构体值)。 - 如果队列已满,无法插入新元素,直接返回
0
表示入队失败。
- 调用之前定义的
-
将元素存入队尾
cQ->base[Q->rear] = e;
- 队列未满时,将元素
e
存入base
数组中rear
指针指向的位置。 - 回忆之前的设计:
rear
始终指向队尾元素的下一个空闲位置 ,因此这里直接赋值即可将e
作为新的队尾元素。
- 队列未满时,将元素
-
更新队尾指针
cQ->rear = (Q->rear + 1) % MAXQSIZE;
- 插入元素后,队尾指针需要向后移动一位,指向新的空闲位置。
- 由于是循环队列,通过
% MAXQSIZE
实现 "绕回" 效果:当rear
达到最大索引(MAXQSIZE-1
)时,加 1 后取模会回到0
,继续使用前面的空闲空间,避免 "假溢出"。
-
返回成功标志
creturn 1;
- 所有操作完成后,返回
1
表示元素e
成功入队。
- 所有操作完成后,返回
循环队列的出队操作
c
int DeQueue(SqQueue *Q, QElemType *e) {
if (QueueEmpty(*Q)) return 0; // 队空
*e = Q->base[Q->front];
Q->front = (Q->front + 1) % MAXQSIZE;
return 1;
}
函数定义:int DeQueue(SqQueue *Q, QElemType *e)
- 函数名
DeQueue
:表示 "出队"(Dequeue,即元素离开队列)。 - 参数
SqQueue *Q
:队列结构体的指针,用于修改队列的状态(如队头指针front
)。 - 参数
QElemType *e
:指向QElemType
类型的指针,用于接收出队的元素值(将队头元素 "带出去")。 - 返回值
int
:1
表示出队成功,0
表示出队失败(队列空时)。
函数内部逻辑:
-
判断队列是否为空
cif (QueueEmpty(*Q)) return 0; // 队空
- 调用之前定义的
QueueEmpty
函数判断队列是否为空(传入*Q
,因为QueueEmpty
需要结构体值)。 - 如果队列为空,没有元素可出队,直接返回
0
表示操作失败。
- 调用之前定义的
-
取出队头元素并通过指针传出
c*e = Q->base[Q->front];
- 队列非空时,队头指针
front
指向的位置就是队头元素(根据队列设计,front
始终指向队头元素)。 - 将
base[front]
(队头元素)赋值给*e
(通过指针e
将元素值传出函数,供外部使用)。
- 队列非空时,队头指针
-
更新队头指针
cQ->front = (Q->front + 1) % MAXQSIZE;
- 移除队头元素后,队头指针需要向后移动一位,指向新的队头元素。
- 同样使用
% MAXQSIZE
实现循环效果:当front
达到最大索引(MAXQSIZE-1
)时,加 1 后取模会绕回0
,继续维护环形结构。
-
返回成功标志
creturn 1;
- 所有操作完成后,返回
1
表示队头元素成功出队。
- 所有操作完成后,返回
严格来说,出队(DeQueue)不会真的把数组里的元素"删除"掉。
出队其实只是"逻辑删除" ,通过移动
front
指针来忽略这个元素,而不是物理删除。
获取队头元素
c
int GetHead(SqQueue Q, QElemType *e) {
if (QueueEmpty(Q)) return 0;
*e = Q.base[Q.front];
return 1;
}
函数定义:int GetHead(SqQueue Q, QElemType *e)
- 函数名
GetHead
:表示 "获取队头"(Get Head),即读取队列的第一个元素。 - 参数
SqQueue Q
:传入队列结构体的值(仅读取队列状态,无需修改队列,因此传值即可)。 - 参数
QElemType *e
:指向QElemType
类型的指针,用于接收队头元素的值(将读取到的队头元素 "带出去" 供外部使用)。 - 返回值
int
:1
表示获取成功(队列非空),0
表示获取失败(队列空,无队头元素)。
函数内部逻辑:
-
判断队列是否为空
cif (QueueEmpty(Q)) return 0;
- 调用
QueueEmpty
函数判断队列是否为空:若为空(front == rear
),则没有队头元素可获取,返回0
表示失败。
- 调用
-
读取队头元素并通过指针传出
c*e = Q.base[Q.front];
- 队列非空时,根据循环队列的设计,
front
指针始终指向队头元素 ,因此直接通过Q.base[Q.front]
即可访问队头元素。 - 将读取到的队头元素赋值给
*e
(通过指针e
传出,供函数外部使用)。
- 队列非空时,根据循环队列的设计,
-
返回成功标志
creturn 1;
- 成功读取队头元素后,返回
1
表示操作成功。
- 成功读取队头元素后,返回
核心特点:仅读取,不修改队列
与出队操作 DeQueue
的关键区别在于:
GetHead
只读取 队头元素,不会改变队列的结构(front
指针位置不变,队列中的元素数量也不变);DeQueue
会移除 队头元素,并修改front
指针位置(队列元素数量减少)。
链式队列
链式队列的概念
队列:先进先出(FIFO)。
顺序队列:用数组实现,空间固定。
链式队列:用链表实现,动态分配内存,理论上只要内存够就不会溢出。
链式队列一般用 单链表 来实现,通常带有 头指针(front) 和 尾指针(rear):
front
:指向队头结点(一般是头结点,方便操作)。rear
:指向队尾结点,方便在队尾插入。
链式队列的基本原理
链式队列通过链表的方式存储数据,遵循 "先进先出(FIFO)" 原则:
- 入队(添加元素):在队尾(
rear
指向的结点后)插入新结点,更新rear
指针。 - 出队(删除元素):移除队头(
front
指向的头结点的下一个结点)元素,更新front
指针的指向。
链式队列的定义
c
// 队列结点
typedef struct QNode {
int data;
struct QNode *next;
} QNode;
// 队列(带头结点)
typedef struct {
QNode *front; // 队头指针
QNode *rear; // 队尾指针
} LinkQueue;
QNode
是队列的基本组成单元(结点),用于存储队列中的单个元素。data
:存储具体的数据(此处定义为int
类型,实际可根据需求修改)。next
:是一个指向QNode
类型的指针,用于连接下一个结点,形成链表结构。LinkQueue
是队列的管理结构,用于整体管理队列(通过队头和队尾指针操作队列)。front
:指向队列的头结点(或队头元素,取决于实现细节,带头结点时通常指向头结点)。rear
:指向队列的最后一个元素(队尾结点)。
链式队列的初始化
c
void InitQueue(LinkQueue *Q) {
QNode *dummy = (QNode *)malloc(sizeof(QNode)); // 建立头结点
dummy->next = NULL;
Q->front = Q->rear = dummy; // front和rear都指向头结点
}
InitQueue(LinkQueue *Q)
的作用是对一个 LinkQueue
类型的队列进行初始化,使其成为一个空队列(但包含头结点),为后续的入队、出队等操作做好准备。
-
创建头结点
cQNode *dummy = (QNode *)malloc(sizeof(QNode)); // 建立头结点
- 通过
malloc
动态分配一块QNode
大小的内存,创建一个头结点(用dummy
指针临时指向)。 - 头结点是一个不存储实际数据的结点,仅用于简化队列操作(例如统一空队列和非空队列的处理逻辑)。
- 通过
-
初始化头结点的指针
cdummy->next = NULL;
- 头结点的
next
指针设为NULL
,表示此时头结点后面没有任何元素(队列初始为空)。
- 头结点的
-
设置队头和队尾指针
cQ->front = Q->rear = dummy; // front和rear都指向头结点
- 让队列的
front
(队头指针)和rear
(队尾指针)都指向刚创建的头结点。 - 这是 "带头结点的空队列" 的标准状态:此时队列中没有有效数据元素,
front
和rear
重合且都指向头结点。
- 让队列的
初始化完成后,队列处于空队列状态:
- 存在一个头结点(不存储数据)。
front
和rear
指针均指向头结点。- 头结点的
next
为NULL
(表示没有后续元素)。
链式队列的判空操作
c
int QueueEmpty(LinkQueue Q) {
return Q.front == Q.rear;
}
- 函数定义 :
int QueueEmpty(LinkQueue Q)
- 函数名
QueueEmpty
直观表示 "队列是否为空"。 - 参数
LinkQueue Q
表示要判断的链式队列(LinkQueue
是链式队列的结构体类型,通常包含队头指针front
和队尾指针rear
)。 - 返回值类型
int
:在 C 语言中常用1
表示 "真"(空队列),0
表示 "假"(非空队列)。
- 函数名
- 函数逻辑 :
return Q.front == Q.rear;
- 核心判断:比较队列的队头指针(front) 和队尾指针(rear) 是否指向同一个位置。
- 在链式队列的典型实现中,当队头指针和队尾指针相等时,意味着队列中没有元素(为空);反之则队列非空。
链式队列的"判满":
- 顺序队列 :因为数组长度固定,所以可以判断
rear == MAXSIZE-1
来判满。- 链式队列 :用的是 动态内存分配,理论上只要系统有足够的内存就不会满。
所以链式队列通常 不需要判满。
链式队列的入队操作
c
void EnQueue(LinkQueue *Q, int x) {
QNode *node = (QNode *)malloc(sizeof(QNode));
node->data = x;
node->next = NULL;
Q->rear->next = node; // 原队尾指向新节点
Q->rear = node; // 更新队尾指针
}
-
创建新结点
cQNode *node = (QNode *)malloc(sizeof(QNode));
- 用
malloc
动态分配一块QNode
大小的内存,创建一个新结点(node
指针指向该结点),用于存储要入队的数据x
。
- 用
-
初始化新结点
cnode->data = x; // 新结点的数据域存入 x node->next = NULL; // 新结点的 next 设为 NULL(因为它将成为队尾,后面暂无其他结点)
-
将新结点链接到队尾
cQ->rear->next = node; // 原队尾结点的 next 指向新结点
- 队列当前的队尾是
Q->rear
指向的结点,通过Q->rear->next = node
,将新结点链接到原队尾结点的后面,使其成为队列的新尾部。
- 队列当前的队尾是
-
更新队尾指针
cQ->rear = node; // 队尾指针 rear 移动到新结点(新结点成为新的队尾)
假设队列初始状态为空队列 (front
和 rear
都指向头结点):
- 第一次入队(如
EnQueue(Q, 10)
):- 创建存储
10
的新结点node1
。 - 头结点(
Q->rear
此时指向头结点)的next
指向node1
。 Q->rear
改为指向node1
(现在node1
是队尾)。
- 创建存储
- 第二次入队(如
EnQueue(Q, 20)
):- 创建存储
20
的新结点node2
。 - 当前队尾
node1
的next
指向node2
。 Q->rear
改为指向node2
(node2
成为新队尾)。
- 创建存储
此时队列结构为:
头结点 -> node1(10) -> node2(20)
,front
仍指头结点,rear
指向 node2
。
链式队列的出队操作
c
int DeQueue(LinkQueue *Q, int *x) {
if (Q->front == Q->rear) return 0; // 空队列
QNode *p = Q->front->next; // 第一个有效结点
*x = p->data;
Q->front->next = p->next; // 头结点指向下一个
if (Q->rear == p) { // 若出队的是最后一个结点
Q->rear = Q->front; // rear回到头结点
}
free(p);
return 1;
}
-
判断队列是否为空
cif (Q->front == Q->rear) return 0; // 空队列
- 若队头指针
front
和队尾指针rear
指向同一位置(即空队列),无法出队,返回 0 表示失败。
- 若队头指针
-
定位队头有效元素
cQNode *p = Q->front->next; // 第一个有效结点
front
指向头结点,头结点的next
才是队列中第一个存储有效数据的结点(队头元素),用p
指针临时指向该结点(即要删除的结点)。
-
获取出队元素的值
c*x = p->data;
- 将待删除结点
p
中存储的数据存入x
指向的内存(调用者通过x
即可获取出队元素)。
- 将待删除结点
-
移除队头元素
cQ->front->next = p->next; // 头结点指向下一个
- 让头结点的
next
跳过p
结点,直接指向p
的下一个结点(p->next
),相当于将p
从链表中 "摘除",此时队列的新队头变为原第二个元素。
- 让头结点的
-
处理 "删除最后一个元素" 的边界情况
cif (Q->rear == p) { // 若出队的是最后一个结点 Q->rear = Q->front; // rear回到头结点 }
- 当
p
是队列中最后一个有效元素(即rear
也指向p
)时,删除p
后队列变为空队列。 - 此时需将
rear
重新指向头结点(与front
保持一致),符合 "空队列时front == rear
" 的规则。
- 当
-
释放内存并返回成功标志
cfree(p); // 释放被删除结点的内存,避免泄漏 return 1; // 出队成功
假设队列当前状态为:
头结点(front) -> node1(10) -> node2(20) -> node3(30)(rear)
- 第一次出队 (删除 node1):
p
指向 node1,*x
被赋值为 10。- 头结点的
next
改为指向 node2。 - 此时
rear
仍指向 node3(非最后一个元素),无需调整rear
。 - 释放 node1 内存,队列变为:
头结点 -> node2(20) -> node3(30)(rear)
。
- 继续出队直到只剩最后一个元素 (删除 node2、node3):
- 删除 node3 时,
p
指向 node3,且rear == p
(node3 是最后一个元素)。 - 头结点的
next
改为NULL
(因为 node3 的next
是 NULL)。 - 执行
Q->rear = Q->front
,rear
回到头结点。 - 释放 node3 内存,队列变为空队列(
front == rear
都指向头结点)。
- 删除 node3 时,