目录
作者:曾把
PostOrder里写成两次InOrder、调试到怀疑人生的东岸
目标读者:已经理解堆和完全二叉树、准备攻克链式二叉树的大一/大二同学
一句话预告:递归不是魔法,而是“分而治之”的思维艺术;遍历不是背代码,而是模拟计算机的思考路径。
🌟 引言:从“数组堆”到“链式树”——为什么还要学链式?
前两篇,我们用数组 + 完全二叉树搞定了堆,效率高、实现快。
但现实中的树,真的都“完全”吗?
比如:
- 一个公司架构图,CEO 下只有 2 个 VP,而某个 VP 下有 10 个经理
- 一棵表达式树:
+的左子是*,右子是5,而*下又有2和3
这些树形状不规则,用数组存会浪费大量空间,甚至无法对齐。
于是,我们回到最灵活的表示法——链式二叉树。
今天,我们将:
- 手动构建一棵链式二叉树
- 深入理解前/中/后序遍历的递归本质
- 掌握求节点数、高度、查找、销毁等基础操作
- 用队列实现层序遍历,并判断是否为完全二叉树
- 带你入门 LeetCode 经典题:单值树、对称树
准备好了吗?递归之旅,现在开始!

1️⃣ 链式二叉树:三个字段,撑起整棵树
🔧 结构定义(二叉链)
每个节点只关心三件事:
- 自己存什么数据(
val) - 左孩子是谁(
left) - 右孩子是谁(
right)
C代码
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType val;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
} BTNode;
✅ 为什么叫“二叉链”?
因为每个结点有两个“链”(指针)连向孩子,像两条腿。
🛠️ 手动建树:从“画图”到“写代码”
我们构建如下这棵树:
1
/ \
2 4
/ / \
3 5 6
/
7
步骤:
- 为每个值创建节点
- 用
->left/->right连起来
BTNode* BuyNode(int val) {
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
node->val = val;
node->left = node->right = NULL;
return node;
}
BTNode* CreateTree() {
BTNode* n1 = BuyNode(1);
BTNode* n2 = BuyNode(2);
BTNode* n3 = BuyNode(3);
BTNode* n4 = BuyNode(4);
BTNode* n5 = BuyNode(5);
BTNode* n6 = BuyNode(6);
BTNode* n7 = BuyNode(7);
n1->left = n2; n1->right = n4;
n2->left = n3;
n4->left = n5; n4->right = n6;
n5->left = n7;
return n1; // 返回根节点
}
💡 小建议:
初学时,先在纸上画树,再写代码连线。否则很容易搞反左右!
2️⃣ 三大遍历:递归的灵魂三问
记住:
所有二叉树的递归操作,都基于“根 + 左子树 + 右子树”这个结构定义。
🔸 前序遍历(Preorder):根 → 左 → 右
访问顺序:先处理自己,再递归左右
void PreOrder(BTNode* root) {
if (root == NULL) {
printf("N "); // 用 N 表示空,方便反推结构
return;
}
printf("%d ", root->val); // 1. 访问根
PreOrder(root->left); // 2. 遍历左子树
PreOrder(root->right); // 3. 遍历右子树
}
输出:1 2 3 N N N 4 5 7 N N N 6 N N
📌 关键:每遇到一个节点,立刻打印。
🔸 中序遍历(Inorder):左 → 根 → 右
访问顺序:先搞定左子树,再处理自己,最后右子树
void InOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
InOrder(root->left); // 1. 左子树
printf("%d ", root->val); // 2. 访问根
InOrder(root->right); // 3. 右子树
}
输出:N 3 N 2 N 1 N 7 N 5 N 4 N 6 N
💡 应用:二叉搜索树(BST)的中序遍历 = 升序序列!
🔸 后序遍历(Postorder):左 → 右 → 根
访问顺序:先把左右孩子搞定,最后处理自己
void PostOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
PostOrder(root->left); // 1. 左
PostOrder(root->right); // 2. 右
printf("%d ", root->val); // 3. 访问根
}
输出:N N 3 N 2 N N N 7 N N 5 N N 6 4 1
⚠️ 踩坑经历:
我第一次写后序,手滑复制了InOrder两遍,结果死活不对。
务必检查函数名!
3️⃣ 递归的本质:计算机如何“思考”遍历?
很多同学背了遍历代码,但不知道为什么这样写。
我们以 PreOrder(1) 为例,模拟函数调用栈:
调用 PreOrder(1)
→ 打印 1
→ 调用 PreOrder(2)
→ 打印 2
→ 调用 PreOrder(3)
→ 打印 3
→ PreOrder(NULL) → 打印 N,返回
→ PreOrder(NULL) → 打印 N,返回
→ 调用 PreOrder(NULL) → 打印 N,返回
→ 调用 PreOrder(4)
→ 打印 4
→ 调用 PreOrder(5)
→ 打印 5
→ 调用 PreOrder(7) → 打印 7 → 两个 N
→ PreOrder(NULL) → N
→ 调用 PreOrder(6) → 打印 6 → 两个 N
关键理解:
递归 = 重复“处理根 + 递归子树”,而系统用栈自动保存每次的状态。
✅ 动手建议:
拿一张纸,手动展开递归过程。这是理解树递归的唯一捷径!
4️⃣ 常见操作:全部用递归实现!
(1)求节点总数
int BinaryTreeSize(BTNode* root) {
if (root == NULL) return 0;
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
📌 思维:总节点 = 自己 + 左子树节点数 + 右子树节点数
(2)求叶子节点数(度为 0)
int BinaryTreeLeafSize(BTNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL)
return 1; // 是叶子
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
(3)求第 k 层节点数
int BinaryTreeLevelKSize(BTNode* root, int k) {
if (root == NULL || k < 1) return 0;
if (k == 1) return 1; // 到目标层了
// 否则:左子树第 k-1 层 + 右子树第 k-1 层
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
(4)求树的高度(深度)
int BinaryTreeDepth(BTNode* root) {
if (root == NULL) return 0;
int leftH = BinaryTreeDepth(root->left);
int rightH = BinaryTreeDepth(root->right);
return (leftH > rightH ? leftH : rightH) + 1;
}
✅ 时间复杂度:O(n),每个节点访问一次
(5)查找值为 x 的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
if (root == NULL) return NULL;
if (root->val == x) return root;
// 在左子树找
BTNode* left = BinaryTreeFind(root->left, x);
if (left) return left;
// 左子树没找到,去右子树
return BinaryTreeFind(root->right, x);
}
(6)销毁二叉树(⚠️ 需要二级指针!)
void BinaryTreeDestroy(BTNode** root) {
if (*root == NULL) return;
BinaryTreeDestroy(&(*root)->left);
BinaryTreeDestroy(&(*root)->right);
free(*root);
*root = NULL; // 避免野指针
}
❗ 为什么用二级指针?
因为要修改root本身(设为 NULL),而不是它指向的内容。
5️⃣ 层序遍历:广度优先,用队列来帮忙
前中后序是深度优先(一条路走到黑),
层序遍历是广度优先(一层一层扫)。
🧠 为什么需要队列?
思路:
把当前层的节点按顺序入队,出队时把它的孩子入队,这样自然就按层访问了。
void LevelOrder(BTNode* root) {
if (root == NULL) return;
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q)) {
BTNode* front = QueueFront(&q);
printf("%d ", front->val);
QueuePop(&q);
if (front->left) QueuePush(&q, front->left);
if (front->right) QueuePush(&q, front->right);
}
QueueDestroy(&q);
}
输出:1 2 4 3 5 6 7
✅ 层序遍历 = BFS(广度优先搜索)
6️⃣ 判断是否为完全二叉树(面试高频!)
🤔 完全二叉树的层序特征?
完全二叉树的层序遍历中,一旦出现 NULL,后面不能再有非 NULL 节点!
算法步骤:
- 按层序遍历思路,连 NULL 也入队
- 一旦遇到
front == NULL,跳出循环 - 检查队列剩余元素:如果还有非 NULL,就不是完全二叉树
bool BinaryTreeComplete(BTNode* root) {
if (root == NULL) return true;
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q)) {
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL) break; // 遇到空,停止入队
// 即使孩子是 NULL,也入队!
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 检查剩余:不能有非空节点
while (!QueueEmpty(&q)) {
BTNode* node = QueueFront(&q);
QueuePop(&q);
if (node != NULL) {
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
📌 关键:NULL 之后不能有非 NULL,这是完全二叉树的“紧凑性”体现。
7️⃣ LeetCode 入门题:思路引导,不是答案
🔸 题1:单值二叉树(LeetCode 965 )
题意:所有节点值都相同?
✅ 递归思路:
- 空树 → true
- 当前节点值 ≠ 左孩子值 → false
- 当前节点值 ≠ 右孩子值 → false
- 递归检查左右子树
💡 边界:孩子为空时,不算“不等”
题2:对称二叉树(LeetCode 101 )
题意:树是否左右镜像对称?
✅ 核心思想:
不是比较左子树和右子树是否相等,而是比较“左的左” vs “右的右”、“左的右” vs “右的左”
bool _isSymmetric(BTNode* left, BTNode* right) {
if (!left && !right) return true;
if (!left || !right) return false;
if (left->val != right->val) return false;
return _isSymmetric(left->left, right->right)
&& _isSymmetric(left->right, right->left);
}
bool isSymmetric(BTNode* root) {
if (!root) return true;
return _isSymmetric(root->left, root->right);
}
这是对递归思维的进阶考验!
✅ 本篇小结
| 遍历 | 前/中/后序 | 递归三段式,注意空指针 |
| 节点数/高度 | 递归分治 | 总 = 左 + 右 + 自己 |
| 查找/销毁 | 递归 + 二级指针 | 销毁要置 NULL |
| 层序遍历 | 队列 + BFS | 连 NULL 也要处理(判断完全树时) |
| 完全二叉树判断 | 层序 + NULL 检查 | NULL 后不能有非 NULL |
| LeetCode 思维 | 递归变形 | 对称 = 镜像递归 |
💡 思考题(动手更深刻!)
- 画递归树:对
PostOrder遍历上面那棵 7 个节点的树,画出函数调用栈的变化过程。 - 反推结构:已知前序
1 2 3 N N N 4 N N,中序N 3 N 2 N 1 N 4 N,画出原树。 - 改写非递归:尝试用栈手动实现中序遍历(进阶挑战!)。
- 完全树判断:以下层序序列是否为完全二叉树?
- A:
[1,2,3,4,5,6] - B:
[1,2,3,4,null,5,6] - C:
[1,2,3,null,4,5,6]
- A:
🌈 终章寄语:树与递归,是数据结构的“成人礼”
从文件系统到堆排序,从遍历到对称树,
二叉树是我们第一次真正接触非线性结构,
递归是我们第一次真正理解分而治之。
你可能会觉得递归“绕”,但请相信:
每一个递归高手,都曾被栈溢出折磨过。
多画图、多模拟、多调试,
总有一天,你会看着一棵树,
心里自动浮现出三种遍历结果,
脑子里自然跳出递归代码。
那时,你就真正“看见”了树。
📢 三连收官:
如果这三篇博客帮到了你,
点赞 + 收藏 + 关注,
是对我最大的支持!

链式二叉树与递归详解



1万+

被折叠的 条评论
为什么被折叠?



