目录
我们放慢节奏,把这个问题拆解成最基本、最独立的部分,然后再把它们组合起来。
很多人感觉"队列结合树就不会写了",这很正常。根本原因是我们往往试图一次性理解两个概念的互动。
根据第一性原理,正确的做法是:先将每个概念彻底分解,独立理解,然后再看它们如何以最简单的方式连接。
数据结构:用链表实现队列(Implementing Queue Using List)-CSDN博客
数据结构:层序遍历 (Level-order Traversal)-CSDN博客
所以,我们的计划是:
-
第一步:彻底忘记树。 我们先从零开始,推导出什么是"链式队列",以及如何实现它。把它当作一个独立的工具来学习。
-
第二步:回归树的遍历问题。 我们重新审视"层序遍历"这个任务的本质需求,看看它到底需要一个什么样的"工具"。
-
第三步:连接。 把我们打造好的"工具"(链式队列)应用到我们的"任务"(层序遍历)上,你会发现一切都水到渠成。
打造我们的工具------从零推导链式队列
问题的起点:为什么要用"链式"?
我们之前用数组实现过队列,它很好用,但有个天生的缺陷:容量固定。
#define MAX_QUEUE_SIZE 100
这行代码意味着队列最多只能装100个元素。如果树很大,有101个节点在同一层,程序可能就出错了。
我们希望能有一个"用多少,装多少",容量可以动态增长的队列。
怎么实现动态增长?在C/C++里,最基础的动态内存技术就是 malloc
和指针,也就是链表。
核心思想:如何用链表模拟"排队"?
一个队列,就是一个排队队伍。它有两个最重要的操作:
-
入队 (Enqueue): 一个人(新元素)站到队尾。
-
出队 (Dequeue): 队头的人(最早来的元素)办理业务,离开队伍。
为了高效地实现这两个操作,我们需要能瞬间定位到队头和队尾。
-
如果只用一个
head
指针指向链表头部,要找到队尾就必须遍历整个链表,效率太低。 -
所以, 一个高效的链式队列,必须有两个指针:一个
front
指针指向队头,一个rear
指針指向队尾。
代码实现
定义队列的"节点"
队列里要存放的是树的节点指针 (Node*
)。所以,队列自己的节点 QueueNode
结构就需要包含两部分:
a. 要存放的数据 (Node* data
)
b. 指向下一个队列节点的指针 (struct QueueNode* next
)
cpp
// 首先,这是我们熟悉的树节点
typedef struct TreeNode {
char data;
struct TreeNode* left;
struct TreeNode* right;
} Node; // 为了方便,我们还是叫它 Node
// 这是队列自己的节点结构
typedef struct QueueNode {
Node* data; // 存放的数据是树节点的指针
struct QueueNode* next; // 指向下一个队列节点
} QueueNode;
定义"队列"本身
队列本身的管理结构,只需要包含我们刚才推导出的两个核心指针:front
和 rear
。
cpp
typedef struct Queue {
QueueNode* front;
QueueNode* rear;
} Queue;
实现队列的基本操作
- 创建队列 (
createQueue
) 一个空的队列,就是队头和队尾指针都为NULL
。
cpp
Queue* createQueue() {
Queue* q = (Queue*)malloc(sizeof(Queue));
q->front = NULL;
q->rear = NULL;
return q;
}
入队 (enqueue
) - 这是最关键的一步,我们放慢点
当一个新元素(比如树节点A
)要入队时:
-
肯定要先为这个新元素创建一个队列节点
newNode
。 -
然后要考虑两种情况:
-
情况一:队列当前是空的 (
q->rear == NULL
) 这时队伍里一个人都没有。新来的newNode
既是队头,也是队尾。所以front
和rear
都应该指向newNode
。 -
情况二:队列里已经有人了
rear
指针正指向当前的队尾。我们要做的是:让当前的队尾的next
指针指向newNode
,然后更新rear
指针,让它指向新的队尾newNode
。
-
cpp
void enqueue(Queue* q, Node* treeNode) {
// 1. 为新来的树节点创建一个队列节点
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = treeNode;
newNode->next = NULL;
// 2. 情况一:队列为空
if (q->rear == NULL) {
q->front = newNode;
q->rear = newNode;
return;
}
// 3. 情况二:队列不为空
q->rear->next = newNode; // 让旧队尾指向新节点
q->rear = newNode; // 更新队尾指针
}
出队 (dequeue
) - 同样放慢
出队就是让队头元素离开。
-
首先检查队列是否为空。如果为空,无队可出。
-
如果不为空,
front
正指向要离开的那个节点。我们要做的就是:
-
先把这个要离开的节点存起来,因为我们待会要返回它里面的数据。
-
将
front
指针向后移动一位,指向它的下一个节点front->next
。 -
一个非常重要的边界情况: 如果出队的是队伍里最后一个人,那么出队后,队列就空了。这时
front
会变成NULL
。此时,我们必须同时把rear
也更新为NULL
!否则rear
就会成为一个指向无效内存的"野指针"。 -
返回我们存起来的那个节点里的数据。
cpp
Node* dequeue(Queue* q) {
// 1. 检查队列是否为空
if (q->front == NULL) {
return NULL;
}
// 2. 保存队头节点和它里面的数据
QueueNode* temp = q->front;
Node* treeNodeData = temp->data;
// 3. 移动 front 指针
q->front = q->front->next;
// 4. 处理边界情况:如果出队后队列变空
if (q->front == NULL) {
q->rear = NULL;
}
// 5. 释放出队的这个队列节点本身的内存,并返回树节点数据
free(temp);
return treeNodeData;
}
检查队列是否为空 (isQueueEmpty
)
这个最简单,看 front
指针是不是 NULL
就行了。
cpp
int isQueueEmpty(Queue* q) {
return q->front == NULL;
}
到此为止,我们已经成功打造了一个功能完备、独立可靠的"链式队列"工具。
你现在可以彻底忘记它的内部实现,只需要记住它有三个接口:enqueue
(在队尾放东西)、dequeue
(从队头取东西)、isQueueEmpty
(判断是不是空的)。
回归任务------层序遍历的本质需求
现在,我们把队列放一边,重新思考"层序遍历"这个任务。 它的口语化描述是: "先访问第0层,再访问第1层,再访问第2层..."
我们手动模拟一下:
-
我们站在树的根节点
A
(第0层)。我们访问它 (打印A
)。 -
好了,
A
访问完了。接下来该访问谁?是第1层的所有节点:B
和C
。 -
核心问题: 当我们在处理
A
的时候,我们怎么"记录"下B
和C
是"待办事项"?并且,我们要保证B
在C
之前被访问。 -
当我们访问
B
的时候,我们又发现了新的待办事项:D
和E
。这些事项的办理顺序应该在C
之后。 -
这个过程像什么?
-
访问
A
,产生待办B
,C
。待办列表:[B, C]
-
从列表头取出
B
访问,产生待办D
,E
。把它们加到列表尾部。待办列表:[C, D, E]
-
从列表头取出
C
访问,产生待办F
。加到列表尾部。待办列表:[D, E, F]
-
...
序遍历的本质,就是一个"先进先出"的任务处理过程。它需要的工具,正是一个队列 (Queue)。
连接!用我们的队列完成遍历
现在,是时候把工具和任务连接起来了。这个过程非常简单,因为我们已经分别把两边都想清楚了。
算法逻辑:
-
创建一个队列。
-
如果树不是空的,把树的根节点入队 (enqueue)。
-
只要队列不为空 (isQueueEmpty),就循环执行以下操作:
a. 从队列中出队 (dequeue) 一个树节点,我们叫它 current
。
b. 访问 current
节点(比如打印它的数据)。
c. 检查 current
的孩子们: * 如果它有左孩子,把左孩子入队 (enqueue)。 * 如果它有右孩子,把右孩子入队 (enqueue)。
你会发现,下面的代码几乎就是上面算法逻辑的逐字翻译。我们完全不需要关心 enqueue
和 dequeue
内部是怎么用链表实现的,我们只管用它们提供的功能。
cpp
void levelOrder(Node* root) {
// 边界情况:如果树是空的,直接返回
if (root == NULL) {
return;
}
// 1. 创建一个队列
Queue* q = createQueue();
// 2. 把根节点入队
enqueue(q, root);
// 3. 只要队列不为空,就循环
while (!isQueueEmpty(q)) {
// a. 出队一个节点
Node* current = dequeue(q);
// b. 访问它
printf("%c ", current->data);
// c. 把它的孩子们(如果存在)加入队列
if (current->left != NULL) {
enqueue(q, current->left);
}
if (current->right != NULL) {
enqueue(q, current->right);
}
}
// 别忘了释放队列本身的内存
free(q);
}
完整代码与回顾
现在,让我们把所有部分(树的创建、链式队列的完整实现、层序遍历函数)放在一起,形成一个可以运行的完整程序。
cpp
#include <stdio.h>
#include <stdlib.h>
// --- 树的节点定义 ---
typedef struct TreeNode {
char data;
struct TreeNode* left;
struct TreeNode* right;
} Node;
// --- 链式队列的节点定义 ---
typedef struct QueueNode {
Node* data;
struct QueueNode* next;
} QueueNode;
// --- 链式队列本身的定义 ---
typedef struct Queue {
QueueNode* front;
QueueNode* rear;
} Queue;
// --- 队列操作函数的实现 ---
Queue* createQueue() {
Queue* q = (Queue*)malloc(sizeof(Queue));
q->front = q->rear = NULL;
return q;
}
int isQueueEmpty(Queue* q) {
return q->front == NULL;
}
void enqueue(Queue* q, Node* treeNode) {
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = treeNode;
newNode->next = NULL;
if (q->rear == NULL) {
q->front = q->rear = newNode;
return;
}
q->rear->next = newNode;
q->rear = newNode;
}
Node* dequeue(Queue* q) {
if (isQueueEmpty(q)) return NULL;
QueueNode* temp = q->front;
Node* treeNodeData = temp->data;
q->front = q->front->next;
if (q->front == NULL) {
q->rear = NULL;
}
free(temp);
return treeNodeData;
}
// --- 层序遍历的实现 ---
void levelOrder(Node* root) {
if (root == NULL) return;
Queue* q = createQueue();
enqueue(q, root);
while (!isQueueEmpty(q)) {
Node* current = dequeue(q);
printf("%c ", current->data);
if (current->left != NULL) {
enqueue(q, current->left);
}
if (current->right != NULL) {
enqueue(q, current->right);
}
}
free(q);
}
// --- 用于测试的辅助函数 ---
Node* createTreeNode(char data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->left = newNode->right = NULL;
return newNode;
}
// --- Main 函数 ---
int main() {
// 构建示例树
Node* root = createTreeNode('A');
root->left = createTreeNode('B');
root->right = createTreeNode('C');
root->left->left = createTreeNode('D');
root->left->right = createTreeNode('E');
root->right->right = createTreeNode('F');
printf("Level-order Traversal (with Linked Queue):\n");
levelOrder(root);
printf("\n");
return 0;
}
回顾一下我们的思路: 我们并没有直接去"写队列和树结合的代码"。我们是先独立地把"链式队列"这个工具造好、想透彻。然后,我们再独立地分析"层序遍历"这个任务,发现它天生就需要一个FIFO的工具。最后一步,我们只是简单地把工具的接口和任务的需求对接起来。
通过这种"隔离-分析-连接"的思路,复杂问题就被分解成了几个简单问题,你就可以更有信心地写出正确的代码了。