一、链式队列的结构
1.1 为什么需要链式队列
顺序队列(循环队列)有容量限制,如果不知道最大元素个数,可能会溢出。链式队列用链表存储,动态分配内存,没有容量限制。
1.2 链式队列的结构
链式队列需要两个指针:
-
front:指向队头节点(出队的一端) -
rear:指向队尾节点(入队的一端)
text
front → [A|next] → [B|next] → [C|next] → NULL
↑
rear
为了操作方便,我们使用带头结点的方式,让空队列和非空队列的操作统一。
1.3 节点和队列定义
c
// 队列节点
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode, *PQueueNode;
// 链式队列
typedef struct {
PQueueNode front; // 队头指针(指向头结点)
PQueueNode rear; // 队尾指针
int size; // 元素个数
} LinkQueue;
二、链式队列的实现
2.1 初始化
创建头结点,front和rear都指向它。
c
void initQueue(LinkQueue *queue) {
queue->front = (PQueueNode)malloc(sizeof(QueueNode));
if (queue->front == NULL) {
printf("初始化失败\n");
exit(1);
}
queue->front->next = NULL;
queue->rear = queue->front;
queue->size = 0;
}
2.2 判断空队列
c
int isEmpty(LinkQueue *queue) {
return queue->front == queue->rear;
// 或者 return queue->size == 0;
}
2.3 入队(enqueue)
在队尾插入新节点。
c
int enqueue(LinkQueue *queue, int value) {
PQueueNode newNode = (PQueueNode)malloc(sizeof(QueueNode));
if (newNode == NULL) {
printf("内存分配失败\n");
return -1;
}
newNode->data = value;
newNode->next = NULL;
queue->rear->next = newNode;
queue->rear = newNode;
queue->size++;
return 0;
}
2.4 出队(dequeue)
删除队头节点(头结点后面的第一个节点)。
c
int dequeue(LinkQueue *queue, int *value) {
if (isEmpty(queue)) {
printf("队列为空\n");
return -1;
}
PQueueNode toDelete = queue->front->next;
*value = toDelete->data;
queue->front->next = toDelete->next;
// 如果删除的是最后一个节点,rear需要指向头结点
if (queue->rear == toDelete) {
queue->rear = queue->front;
}
free(toDelete);
queue->size--;
return 0;
}
2.5 获取队头元素
c
int getFront(LinkQueue *queue, int *value) {
if (isEmpty(queue)) {
printf("队列为空\n");
return -1;
}
*value = queue->front->next->data;
return 0;
}
2.6 获取队列大小
c
int getSize(LinkQueue *queue) {
return queue->size;
}
2.7 销毁队列
c
void destroyQueue(LinkQueue *queue) {
PQueueNode cur = queue->front;
while (cur != NULL) {
PQueueNode temp = cur;
cur = cur->next;
free(temp);
}
queue->front = NULL;
queue->rear = NULL;
queue->size = 0;
}
三、链式队列完整代码
c
#include <stdio.h>
#include <stdlib.h>
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode, *PQueueNode;
typedef struct {
PQueueNode front;
PQueueNode rear;
int size;
} LinkQueue;
void initQueue(LinkQueue *queue) {
queue->front = (PQueueNode)malloc(sizeof(QueueNode));
if (queue->front == NULL) {
printf("初始化失败\n");
exit(1);
}
queue->front->next = NULL;
queue->rear = queue->front;
queue->size = 0;
}
int isEmpty(LinkQueue *queue) {
return queue->front == queue->rear;
}
int enqueue(LinkQueue *queue, int value) {
PQueueNode newNode = (PQueueNode)malloc(sizeof(QueueNode));
if (newNode == NULL) return -1;
newNode->data = value;
newNode->next = NULL;
queue->rear->next = newNode;
queue->rear = newNode;
queue->size++;
return 0;
}
int dequeue(LinkQueue *queue, int *value) {
if (isEmpty(queue)) return -1;
PQueueNode toDelete = queue->front->next;
*value = toDelete->data;
queue->front->next = toDelete->next;
if (queue->rear == toDelete) {
queue->rear = queue->front;
}
free(toDelete);
queue->size--;
return 0;
}
int getFront(LinkQueue *queue, int *value) {
if (isEmpty(queue)) return -1;
*value = queue->front->next->data;
return 0;
}
int getSize(LinkQueue *queue) {
return queue->size;
}
void destroyQueue(LinkQueue *queue) {
PQueueNode cur = queue->front;
while (cur != NULL) {
PQueueNode temp = cur;
cur = cur->next;
free(temp);
}
queue->front = NULL;
queue->rear = NULL;
queue->size = 0;
}
void printQueue(LinkQueue *queue) {
printf("队头 -> ");
PQueueNode cur = queue->front->next;
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("<- 队尾 (size=%d)\n", queue->size);
}
int main() {
LinkQueue queue;
initQueue(&queue);
enqueue(&queue, 10);
enqueue(&queue, 20);
enqueue(&queue, 30);
printQueue(&queue);
int val;
dequeue(&queue, &val);
printf("出队: %d\n", val);
printQueue(&queue);
enqueue(&queue, 40);
enqueue(&queue, 50);
printQueue(&queue);
destroyQueue(&queue);
return 0;
}
运行结果:
text
队头 -> 10 20 30 <- 队尾 (size=3)
出队: 10
队头 -> 20 30 <- 队尾 (size=2)
队头 -> 20 30 40 50 <- 队尾 (size=4)
四、队列的应用:广度优先搜索(BFS)
4.1 BFS的核心思想
广度优先搜索(Breadth-First Search)是一种图的遍历算法。它的特点是一层一层地向外扩展,先访问离起点近的节点,再访问远的节点。
为什么用队列?
-
BFS需要按照"先进入的节点先扩展"的顺序
-
队列的FIFO特性完美契合这个需求
4.2 迷宫寻路问题
问题描述:给定一个迷宫,0表示可走,1表示墙,求从起点到终点的最短步数。
算法思路:
-
起点入队,标记已访问
-
当队列不为空时:
-
取出队头节点
-
如果这个节点是终点,返回步数
-
否则,把它上下左右四个方向的邻居中可走且未访问的节点入队,步数+1
-
为什么BFS能找到最短路径?
因为BFS是一层一层扩展的,第一次到达终点时,走过的步数就是最短的。
4.3 代码实现
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_SIZE 100
// 坐标结构体
typedef struct {
int x;
int y;
int step; // 步数
} Point;
// 队列节点(存储Point)
typedef struct QueueNode {
Point data;
struct QueueNode *next;
} QueueNode, *PQueueNode;
typedef struct {
PQueueNode front;
PQueueNode rear;
int size;
} LinkQueue;
void initQueue(LinkQueue *q) {
q->front = (PQueueNode)malloc(sizeof(QueueNode));
q->front->next = NULL;
q->rear = q->front;
q->size = 0;
}
int isEmpty(LinkQueue *q) {
return q->front == q->rear;
}
void enqueue(LinkQueue *q, Point val) {
PQueueNode newNode = (PQueueNode)malloc(sizeof(QueueNode));
newNode->data = val;
newNode->next = NULL;
q->rear->next = newNode;
q->rear = newNode;
q->size++;
}
int dequeue(LinkQueue *q, Point *val) {
if (isEmpty(q)) return -1;
PQueueNode toDelete = q->front->next;
*val = toDelete->data;
q->front->next = toDelete->next;
if (q->rear == toDelete) {
q->rear = q->front;
}
free(toDelete);
q->size--;
return 0;
}
void destroyQueue(LinkQueue *q) {
PQueueNode cur = q->front;
while (cur != NULL) {
PQueueNode temp = cur;
cur = cur->next;
free(temp);
}
}
// 四个方向:上、下、左、右
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
// BFS求最短路径
int bfs(int maze[MAX_SIZE][MAX_SIZE], int n, int m,
Point start, Point end) {
int visited[MAX_SIZE][MAX_SIZE] = {0};
LinkQueue q;
initQueue(&q);
start.step = 0;
enqueue(&q, start);
visited[start.x][start.y] = 1;
while (!isEmpty(&q)) {
Point cur;
dequeue(&q, &cur);
// 到达终点
if (cur.x == end.x && cur.y == end.y) {
destroyQueue(&q);
return cur.step;
}
// 遍历四个方向
for (int i = 0; i < 4; i++) {
int nx = cur.x + dx[i];
int ny = cur.y + dy[i];
// 检查边界和是否可走
if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
maze[nx][ny] == 0 && !visited[nx][ny]) {
visited[nx][ny] = 1;
Point next = {nx, ny, cur.step + 1};
enqueue(&q, next);
}
}
}
destroyQueue(&q);
return -1; // 不可达
}
int main() {
// 5x5迷宫,0=路,1=墙
int maze[MAX_SIZE][MAX_SIZE] = {
{0, 0, 0, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 1, 0},
{0, 1, 0, 0, 0},
{0, 0, 0, 0, 0}
};
Point start = {0, 0, 0};
Point end = {4, 4, 0};
int steps = bfs(maze, 5, 5, start, end);
if (steps != -1) {
printf("从(%d,%d)到(%d,%d)的最短步数: %d\n",
start.x, start.y, end.x, end.y, steps);
} else {
printf("无法到达终点\n");
}
return 0;
}
运行结果:
text
从(0,0)到(4,4)的最短步数: 8
五、顺序队列 vs 链式队列
| 对比项 | 顺序队列(循环队列) | 链式队列 |
|---|---|---|
| 存储方式 | 数组(连续内存) | 链表(非连续) |
| 容量 | 固定,可能溢出 | 动态,受内存限制 |
| 入队 | O(1) | O(1) |
| 出队 | O(1) | O(1) |
| 内存利用率 | 可能浪费一个单元 | 每节点多一个指针 |
| 适用场景 | 大小已知 | 大小未知 |
六、BFS的其他应用
| 应用 | 说明 |
|---|---|
| 迷宫最短路径 | 层序遍历,第一次到达就是最短 |
| 单词接龙 | 从一个单词变换到另一个单词的最少步数 |
| 社交网络 | 找两个人之间的最短关系链 |
| 图的连通分量 | 遍历所有可达节点 |
| 拓扑排序 | 有向无环图的层序遍历 |
七、小结
这一篇我们实现了链式队列,并用BFS解决了迷宫最短路径问题:
| 内容 | 要点 |
|---|---|
| 链式队列 | 用单链表实现,带头结点,front指向头,rear指向尾 |
| 入队 | 在rear后面插入新节点 |
| 出队 | 删除front后面的第一个节点 |
| BFS | 利用队列的FIFO特性,一层一层扩展 |
| 最短路径 | BFS第一次到达终点时的步数就是最短 |
BFS的核心代码模板:
text
起点入队,标记已访问
while (队列不为空) {
取出队头节点
if (是终点) 返回步数
遍历所有邻居:
if (邻居可走且未访问) {
标记已访问
邻居入队,步数+1
}
}
下一篇我们会讲树形结构,从二叉树开始。
八、思考题
-
链式队列为什么需要带头结点?不带头结点会有什么问题?
-
BFS中,为什么第一次到达终点时就是最短路径?DFS能不能保证最短?
-
尝试修改BFS代码,让它输出具体的最短路径(不只是步数)。
-
如果用栈代替队列实现迷宫寻路,会是什么效果?
欢迎在评论区讨论你的答案。