1 前置说明
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在对二叉树结构掌握还不够深入,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,之后再来系统的研究二叉树真正的创建方式。
cs
typedef int BTDataType;
typedef struct BinaryTreeNode
{
//节点数据
BTDataType data;
//左右孩子
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BT;
BT* BuyNode(int x)
{
//扩容
BT* node = (BT*)malloc(sizeof(BT));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
BT* CreatBinaryTree()
{
//节点赋值
BT* node1 = BuyNode(1);
BT* node2 = BuyNode(2);
BT* node3 = BuyNode(3);
BT* node4 = BuyNode(4);
BT* node5 = BuyNode(5);
BT* node6 = BuyNode(6);
//链接节点
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
-
空树
-
非空:根结点,根结点的左子树、根结点的右子树组成的。

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
2 前序遍历
2.1 代码
cpp
//前序遍历
void PrevOrder(BT* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
//递归访问
PrevOrder(root->left);
PrevOrder(root->right);
}
2.2 输出结果以及验证


2.3 执行过程
-
类型定义阶段:
- 首先执行
typedef int BTDataType
,将int
类型重命名为BTDataType
- 接着定义
struct BinaryTreeNode
结构体,并通过typedef
重命名为BT
,完成二叉树节点的类型定义
- 首先执行
-
调用
CreatBinaryTree
函数创建二叉树:- 函数内部依次调用
BuyNode(1)
到BuyNode(6)
创建 6 个节点:- 每个
BuyNode
调用都会:分配内存→检查分配结果→初始化数据和指针→返回节点地址
- 每个
- 节点创建完成后,建立链接关系:
node1->left = node2
(1 的左孩子是 2)node1->right = node4
(1 的右孩子是 4)node2->left = node3
(2 的左孩子是 3)node4->left = node5
(4 的左孩子是 5)node4->right = node6
(4 的右孩子是 6)
- 最后返回根节点
node1
的地址
- 函数内部依次调用
-
调用
PrevOrder
函数进行前序遍历 (以根节点node1
为参数):- 第一层递归(root=node1):
- 输出
1
- 调用
PrevOrder(node1->left)
(即 node2)
- 输出
- 第二层递归(root=node2):
- 输出
2
- 调用
PrevOrder(node2->left)
(即 node3)
- 输出
- 第三层递归(root=node3):
- 输出
3
- 调用
PrevOrder(node3->left)
(即 NULL)→输出N
并返回 - 调用
PrevOrder(node3->right)
(即 NULL)→输出N
并返回 - 返回到第二层
- 输出
- 第二层继续:
- 调用
PrevOrder(node2->right)
(即 NULL)→输出N
并返回 - 返回到第一层
- 调用
- 第一层继续:
- 调用
PrevOrder(node1->right)
(即 node4)
- 调用
- 第二层递归(root=node4):
- 输出
4
- 调用
PrevOrder(node4->left)
(即 node5)
- 输出
- 第三层递归(root=node5):
- 输出
5
- 调用
PrevOrder(node5->left)
(即 NULL)→输出N
并返回 - 调用
PrevOrder(node5->right)
(即 NULL)→输出N
并返回 - 返回到第二层
- 输出
- 第二层继续:
- 调用
PrevOrder(node4->right)
(即 node6)
- 调用
- 第三层递归(root=node6):
- 输出
6
- 调用
PrevOrder(node6->left)
(即 NULL)→输出N
并返回 - 调用
PrevOrder(node6->right)
(即 NULL)→输出N
并返回 - 返回到第二层,再返回到第一层
- 输出
- 遍历结束,最终输出结果:
1 2 3 N N N 4 5 N N 6 N N
- 第一层递归(root=node1):
整个过程从类型定义开始,先创建节点并构建二叉树结构,最后通过递归实现前序遍历并输出结果。
2.4 逻辑过程(程序执行的逻辑流程)
-
类型定义与函数声明
- 逻辑上先完成数据类型的抽象定义:通过
typedef
将int
定义为BTDataType
(数据类型抽象),定义BinaryTreeNode
结构体(描述二叉树节点的逻辑结构:包含数据域和左右孩子指针),并简化命名为BT
。 - 声明
BuyNode
、CreatBinaryTree
、PrevOrder
三个函数,确定函数的输入输出逻辑。
- 逻辑上先完成数据类型的抽象定义:通过
-
二叉树的构建逻辑
- 节点创建 :
BuyNode
函数的逻辑是 "输入一个值→申请内存→初始化节点(数据赋值、指针置空)→返回节点",封装了单个节点的创建逻辑。 - 树结构构建 :
CreatBinaryTree
函数通过逻辑步骤构建树:先创建 6 个独立节点(值 1-6),再通过指针赋值建立节点间的父子关系(如node1->left = node2
表示 1 的左孩子是 2),最终形成一个固定结构的二叉树,逻辑上确定了树的拓扑关系。
- 节点创建 :
-
前序遍历的逻辑流程
- 遵循 "根→左→右" 的递归逻辑:
- 若当前节点为
NULL
,逻辑上表示 "空节点",输出N
; - 否则先输出当前节点数据,再递归处理左子树(逻辑上的 "左"),最后递归处理右子树(逻辑上的 "右");
- 通过递归调用,将整棵树的遍历拆解为单个节点的处理逻辑,最终按前序顺序输出所有节点(包括空节点)。
2.5 物理过程(程序在计算机中的实际执行操作)
-
编译与内存分配
- 编译阶段:类型定义和函数声明被编译器解析,确定
BT
类型的内存大小(int
数据 + 两个指针,通常为 16 字节,取决于系统)。 - 运行时:
BuyNode
函数调用malloc(sizeof(BT))
,向操作系统申请一块连续的内存(物理地址上的一块空间),用于存储节点数据;- 若申请成功,通过指针
node
指向该内存块,然后将参数x
写入数据域,将左右指针域物理地址设为0
(NULL
的物理表示)。
- 编译阶段:类型定义和函数声明被编译器解析,确定
-
二叉树的物理存储
- 6 个节点通过
BuyNode
分别在内存中占据独立的物理块(地址不连续); CreatBinaryTree
中的指针赋值(如node1->left = node2
),本质是将node2
的物理内存地址写入node1
的左指针域,通过物理地址的关联形成 "树结构"(逻辑上的父子关系对应物理上的地址指向)。
- 6 个节点通过
-
前序遍历的物理执行
- 递归调用时,每次调用
PrevOrder
会在栈内存中创建函数栈帧(存储参数root
的物理地址、返回地址等); - 访问
root->data
时,通过root
存储的物理地址找到节点内存块,读取其中的数据域并输出; - 递归访问左 / 右子树时,实际是将左 / 右指针存储的物理地址作为新参数压入栈,重复上述过程;
- 当
root
为NULL
(物理地址0
),输出N
并弹出当前栈帧,返回上一层调用。
- 递归调用时,每次调用
-
最终结果的物理表现
- 所有输出操作通过标准输出流(如控制台)将字符序列
1 2 3 N N N 4 5 N N 6 N N
物理地显示在设备上,完成整个过程。
- 所有输出操作通过标准输出流(如控制台)将字符序列
总结
- 逻辑过程:关注 "做什么",即数据结构的定义、函数的逻辑步骤、递归的执行顺序等抽象流程。
- 物理过程:关注 "怎么做",即内存的分配与释放、指针的地址指向、栈帧的创建与销毁、数据在硬件上的读写等实际操作。
物理上空间是可以重复利用的。
需要注意的是无限向下递归(树的深度太深),则会导致栈溢出的问题。
3 中序遍历
3.1 代码
cpp
//中序遍历
void InOrder(BT* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
3.2 输出结果及其验证


3.3 执行过程
圈起来的数据,表示次序。

4 计算节点个数
4.1 统计二叉树的节点总数
错误代码:


运行结果:

因为在这个过程中,局部静态,初始化0,只会被执行一次,下一次调用的时候,就不能为0了,会在之前的结果上面累加。所以,不可以用静态问题解决。
所以:
cpp
//正确 求个数
int TreeSize(BT* root)
{
return root == NULL ? 0 :
TreeSize(root->left) + TreeSize(root->right) + 1;
}
int main()
{
BT* root = CreatBinaryTree();
printf("TreeSize:%d\n", TreeSize(root));
printf("TreeSize:%d\n", TreeSize(root));
return 0;
}

思路:
- 功能目标:计算二叉树中所有节点(包括根、内部节点、叶子节点)的总数量。
- 分解逻辑 :
- 空树(
root == NULL
)的节点数为 0; - 非空树的节点数 = 1(当前根节点) + 左子树的节点数 + 右子树的节点数。
- 空树(
4.2 统计二叉树的叶子节点数
cpp
//统计二叉树的叶子节点数
int TreeLeafSize(BT* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return TreeLeafSize(root->left)
+ TreeLeafSize(root->right);
}
思路:
- 功能目标:计算二叉树中 "左右孩子均为空" 的节点(叶子节点)数量。
- 分解逻辑 :
- 空树的叶子数为 0;
- 若当前节点是叶子(
left == NULL && right == NULL
),则贡献 1 个叶子; - 非叶子节点的叶子数 = 左子树的叶子数 + 右子树的叶子数(当前节点不贡献叶子)。
4.3 计算二叉树的高度
cs
//计算二叉树的高度
int TreeHeight(BT* root)
{
if (root == NULL)
return 0;
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ?
leftHeight + 1 : rightHeight + 1;
}
思路:
- 功能目标:计算二叉树的最大深度(从根到最远叶子节点的边数 + 1,或节点层数)。
- 分解逻辑 :
- 空树的高度为 0;
- 非空树的高度 = 1(当前节点所在层) + 左右子树中较高者的高度(取最大值保证是 "最远叶子")。

这是「递归的核心」:把当前节点的左、右子树当成独立的小树,分别计算它们的高度。
以示例树为例:

- 计算根节点 1 的左子树(节点 2)的高度:
- 节点 2 的左子树是节点 4,节点 4 的左右子树都是
NULL
- 所以
TreeHeight(节点4)
的结果是 1(自身 1 层 + 左右子树高度 0) - 因此
TreeHeight(节点2)
= 1(自身) +TreeHeight(节点4)
(1) = 2
- 节点 2 的左子树是节点 4,节点 4 的左右子树都是
- 计算根节点 1 的右子树(节点 3)的高度:
- 节点 3 的左右子树都是
NULL
- 所以
TreeHeight(节点3)
= 1(自身 1 层 + 左右子树高度 0)
- 节点 3 的左右子树都是
计算当前节点的高度

- 逻辑:当前节点的高度 = 自身这一层(+1) + 左右子树中「更高的那个子树的高度」
- 为什么取最大值?因为高度要算「最远」的叶子节点,所以选更高的一边。
以示例树为例:
- 根节点 1 的左子树高度是 2(节点 2 的高度),右子树高度是 1(节点 3 的高度)
- 取最大值 2,加 1(自身层),得到 3 → 这就是整棵树的高度
cs
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
这是三目运算符,等价于下面的 if-else 逻辑:
cpp
if (leftHeight > rightHeight) {
// 左子树更高,当前节点高度 = 左子树高度 + 自身这一层
return leftHeight + 1;
} else {
// 右子树更高(或相等),当前节点高度 = 右子树高度 + 自身这一层
return rightHeight + 1;
}
这句代码的作用是 ------在左右子树中选更高的那个高度,再加上当前节点自己这一层,就是当前节点的高度。
为什么要加 1?
"+1" 是因为当前节点自身也算一层 。
比如:
- 如果一个节点的左子树高度是 2(意味着左子树有 2 层),右子树高度是 1
- 那么从当前节点往下数,最深能到左子树的第 2 层,加上当前节点自己这一层,总高度就是 2 + 1 = 3
想象你站在一棵二叉树的某个节点上,想知道从这个节点到它下方最深的叶子有多少层(这就是该节点的 "高度")。
- 你左边有一棵子树,高度是
leftHeight
(比如 3 层) - 你右边有一棵子树,高度是
rightHeight
(比如 2 层) - 那么你所在位置的高度 = 你自己这一层(+1) + 左右子树中更高的那个高度(选左边的 3 层)
- 结果就是:3 + 1 = 4 层
所以:

总结
这三个函数的编写思路高度统一:
- 利用二叉树的递归结构,将整体问题拆解为 "根节点处理"+"左右子树递归处理";
- 明确空节点的边界条件,确保递归终止;
- 根据不同统计目标(节点总数 / 叶子数 / 高度),设计 "当前节点贡献值" 与 "子树结果" 的组合方式(累加 / 条件判断 / 取最大值)。
这种思路既符合二叉树的结构特性,又使代码简洁易懂,充分体现了递归在处理树结构问题时的优势。
运行一下:

注意:
不要

这样写,因为这段代码确实存在严重的效率问题,核心原因是对同一子树进行了冗余的递归计算,导致时间复杂度呈指数级增长,尤其在处理深度较大的树时,效率会极其低下。通过引入临时变量存储中间结果,可以彻底解决这个问题。
我们通过具体分析来理解:
问题根源:重复计算同一子树
假设我们要计算某个节点的高度,这段代码的执行逻辑是:
- 先调用
TreeHeight(root->left)
计算左子树高度(记为 A); - 再调用
TreeHeight(root->right)
计算右子树高度(记为 B); - 比较 A 和 B 的大小后,如果 A 更大,会再次调用
TreeHeight(root->left)
(又算一次 A) ,然后 + 1 返回;
同理,如果 B 更大,会再次调用TreeHeight(root->right)
(又算一次 B),然后 + 1 返回。
也就是说,左子树或右子树会被计算 2 次(一次用于比较,一次用于最终结果)。
对效率的影响:呈指数级放大
以一棵简单的左斜树(每个节点只有左子树)为例:

计算根节点 1 的高度时:
- 正常逻辑(用临时变量):左子树只需计算 1 次,总递归次数为
n
; - 这段代码:左子树会被计算 2 次,而每个左子树的计算又会导致其下一层左子树被计算 2 次,最终总递归次数为
2^n
(指数级增长)。
当树的深度为 20 时,2^20
约为 100 万次计算,而正常逻辑只需 20 次,效率差距悬殊。
所以用leftHeight
和rightHeight
存储子树高度,避免重复调用TreeHeight
(如果直接写max(TreeHeight(left), TreeHeight(right))
,会导致同一子树被计算两次,效率低)。
4.4 二叉树第 k 层的节点个数
cpp
// 二叉树第k层节点个数
int TreeLevelKSize(BT* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
// 子问题
return TreeLevelKSize(root->left, k - 1)
+ TreeLevelKSize(root->right, k - 1);
}
将 "求第 k 层节点数" 的问题分解为:
当前树的第 k 层节点数 = 左子树的第 k-1 层节点数 + 右子树的第 k-1 层节点数
逻辑拆解
-
边界条件处理:
- 若当前节点为
NULL
(空树),则该子树不存在第 k 层节点,返回 0; - 若
k == 1
,说明当前节点就是第 k 层的节点(因为从根节点开始算第 1 层),返回 1。
- 若当前节点为
-
递归分解 :
对于非空节点且
k > 1
时:- 当前节点的左子树的第
k-1
层,就是原树的第k
层(因为左子树比当前节点低一层); - 当前节点的右子树的第
k-1
层,也是原树的第k
层; - 因此,将左、右子树的第
k-1
层节点数相加,就是原树第k
层的总节点数。
- 当前节点的左子树的第
对于当前树的第 k 层节点数 = 左子树的第 k-1 层节点数 + 右子树的第 k-1 层节点数的理解:
用 "层次递进" 的思路来理解这个分解逻辑,结合具体例子一步一步看:
先明确:二叉树的 "层" 是如何定义的?
- 根节点(最顶层的节点)是第 1 层;
- 根节点的直接子节点是第 2 层;
- 第 2 层节点的直接子节点是第 3 层;
- 以此类推,第
n
层节点的子节点是第n+1
层。
核心逻辑:父层与子层的关系
对于任意一个节点,它的左、右子树的 "层次" 比它自身的层次少 1 。
比如:
- 若当前节点在第
m
层,那么它的左、右孩子就在第m+1
层; - 反过来讲:当前树的第
k
层节点,恰好是它左、右子树的第k-1
层节点。
用例子拆解 "分解逻辑"
假设我们有这样一棵二叉树,要计算它的第 3 层节点数:

目标:求整棵树(以 A 为根)的第 3 层节点数
第 3 层的节点是 D、E、F,共 3 个。
分解过程:
-
观察第 3 层节点与上层的关系:
- D 是 B 的左孩子(B 在第 2 层);
- E 是 C 的左孩子,F 是 C 的右孩子(C 在第 2 层)。
所以,整棵树的第 3 层节点 = B 的子树中的第 2 层节点 + C 的子树中的第 2 层节点。
-
为什么是 "k-1"?
- 因为 B 和 C 是 A 的子树(左子树和右子树);
- 对于 B 的子树来说,B 是它的根(第 1 层),所以 D 是 B 的子树的第 2 层(即
k-1=3-1=2
); - 对于 C 的子树来说,C 是它的根(第 1 层),所以 E 和 F 是 C 的子树的第 2 层(也是
k-1=2
)。
-
结论:
整棵树(A 为根)的第 3 层节点数 = B 的子树的第 2 层节点数(D,1 个) + C 的子树的第 2 层节点数(E、F,2 个) = 3 个。
推广到通用情况
对于任意一棵以root
为根的树,求它的第k
层节点数:
- 如果
root
为空,说明没有节点,返回 0; - 如果
k=1
,说明root
本身就是第 1 层节点,返回 1; - 如果
k>1
,则第k
层节点一定在root
的左、右子树中,且是左、右子树的第k-1
层节点(因为左、右子树的根是root
的孩子,比root
低 1 层)。
因此:
当前树的第 k 层节点数 = 左子树的第 k-1 层节点数 + 右子树的第 k-1 层节点数
再用递归步骤验证
还是求上面例子中第 3 层节点数(k=3
):
-
用
TreeLevelKSize(A, 3)
:- A 非空,
k≠1
,所以计算左子树 B 的第 2 层(3-1=2) + 右子树 C 的第 2 层(3-1=2)。
- A 非空,
-
计算
TreeLevelKSize(B, 2)
:- B 非空,
k=2≠1
,计算 B 的左子树 D 的第 1 层(2-1=1) + B 的右子树(空)的第 1 层。 - D 非空,
k=1
,返回 1;右子树为空,返回 0。总和 = 1+0=1。
- B 非空,
-
计算
TreeLevelKSize(C, 2)
:- C 非空,
k=2≠1
,计算 C 的左子树 E 的第 1 层 + C 的右子树 F 的第 1 层。 - E 非空,
k=1
,返回 1;F 非空,k=1
,返回 1。总和 = 1+1=2。
- C 非空,
-
最终结果:1+2=3(正确)。
通过这个过程可以看出,"分解为左、右子树的 k-1 层" 的逻辑,本质是利用了二叉树 "父层与子层差 1" 的特性,将大问题拆解为更小的子问题,最终通过递归一步步求解。
4.5 二叉树查找值为x的节点
cpp
// 二叉树查找值为x的节点
BT* TreeFind(BT* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BT* ret1 = TreeFind(root->left, x);
if (ret1)
return ret1;
BT* ret2 = TreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
这段代码用于在二叉树中查找值为x
的节点并返回该节点的地址,采用递归方式实现了深度优先搜索(先检查当前节点,再遍历左子树,最后遍历右子树),具体逻辑如下:
遵循 "先根后左右" 的查找顺序:
- 先检查当前节点是否为目标节点;
- 若不是,递归查找左子树;
- 若左子树中找到,直接返回结果;
- 若左子树中未找到,再递归查找右子树;
- 若左右子树都未找到,返回
NULL
。
逻辑拆解
-
边界条件处理:
cppif (root == NULL) return NULL;
若当前节点为空(递归到空树),说明该路径上没有目标节点,返回
NULL
。 -
检查当前节点:
csif (root->data == x) return root;
若当前节点的值等于
x
,直接返回当前节点的地址(找到目标)。 -
递归查找左子树:
cppBT* ret1 = TreeFind(root->left, x); if (ret1) return ret1;
先在左子树中查找,用
ret1
存储查找结果;- 若
ret1
不为NULL
(左子树中找到目标节点),直接返回该结果(无需再查右子树)。
- 若
-
递归查找右子树:
cppBT* ret2 = TreeFind(root->right, x); if (ret2) return ret2;
若左子树中未找到(
ret1
为NULL
),再在右子树中查找,用ret2
存储结果;
若ret2
不为NULL
(右子树中找到目标节点),返回该结果。 -
未找到的情况:
cppreturn NULL;
若当前节点、左子树、右子树中都没有值为
x
的节点,返回NULL
示例说明
以如下二叉树为例,查找值为5
的节点:

查找过程:
- 从根节点 1 开始,
1≠5
,递归查找左子树(节点 2)。 - 节点 2 的值
≠5
,递归查找其左子树(节点 3)。 - 节点 3 的值
≠5
,其左右子树为空,返回NULL
(左子树未找到)。 - 回到节点 2,右子树为空,返回
NULL
(左子树整体未找到)。 - 回到根节点 1,递归查找右子树(节点 4)。
- 节点 4 的值
≠5
,递归查找其左子树(节点 5)。 - 节点 5 的值
=5
,返回节点 5 的地址。 - 最终结果:成功返回值为 5 的节点地址。
注意返回的过程也是一层一层向上递归的。
节点 5 的值 = 5 → 直接返回节点 5 的地址给上一层(节点 4)。
TreeFind(4, 5)
接收到左子树返回的 "节点 5 地址"
由于ret1
(左子树结果)不为NULL
,直接返回该地址给上一层(节点 1)。
TreeFind(1, 5)
接收到右子树返回的 "节点 5 地址"
之后,直接返回该地址作为最终结果。
5 二叉树销毁
cpp
// 二叉树销毁
void TreeDestory(BT* root)
{
if (root == NULL)
return;
TreeDestory(root->left);
TreeDestory(root->right);
free(root);
}
依次销毁:

6 层序遍历
cpp
// 层序遍历二叉树
// 从根节点开始,按层次依次访问所有节点
void TreeLevelOrder(BT* root)
{
// 用于暂存当前层的节点,实现按层次访问
Queue q;
QueueInit(&q);
// 初始化队列(具体实现依赖Queue.h中的定义)
// 根节点入队:如果树非空,先将根节点加入队列作为起始点
if (root)
// 处理空树情况
//(root为NULL时,直接跳过入队)
QueuePush(&q, root);
// 将根节点指针存入队列
//循环处理队列中的节点
//直到队列为空(所有节点都访问完毕)
while (!QueueEmpty(&q))
// 队列不为空时,说明还有节点未处理
{
// 取出队头节点:当前层的第一个节点
BT* front = QueueFront(&q);
// 获取队头元素(不删除)
QueuePop(&q);
// 移除队头元素(已访问,不再需要存储)
// 访问当前节点:打印节点数据
printf("%d ", front->data);
// 将当前节点的左右孩子入队:为下一层遍历做准备
if (front->left)
// 左孩子非空时入队
QueuePush(&q, front->left);
if (front->right)
// 右孩子非空时入队
QueuePush(&q, front->right);
// 左右孩子入队的顺序保证了"同一层从左到右"的访问顺序
}
// 4. 遍历结束后销毁队列,释放内存,避免内存泄漏
QueueDestroy(&q);
}
利用队列思想,一层一层的,上一层带下一层。
插入节点:

打印结果:

7 判断二叉树是否是完全二叉树
cpp
// 判断二叉树是否是完全二叉树
bool TreeComplete(BT* root)
{
Queue q;
QueueInit(&q);
// 初始化队列
if (root)
// 若树非空,根节点入队
//(作为遍历起点)
QueuePush(&q, root);
//层序遍历所有节点(包括空节点),直到遇到第一个空节点
while (!QueueEmpty(&q))
{
BT* front = QueueFront(&q);
// 取出队头节点
QueuePop(&q);
// 出队
// 遇到第一个空节点,跳出循环,进入第二阶段检查
if (front == NULL)
{
break;
}
// 关键操作:无论左右孩子是否为空,都入队(记录所有位置,包括空)
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 检查第一个空节点之后的所有节点是否都是空节点
while (!QueueEmpty(&q))
{
BT* front = QueueFront(&q);
// 取出队头节点
QueuePop(&q);
// 出队
// 若存在非空节点
// 说明不符合完全二叉树的连续性,返回false
if (front)
{
QueueDestroy(&q);
// 释放队列内存
return false;
}
}
// 所有节点检查完毕,符合完全二叉树定义
QueueDestroy(&q);
// 释放队列内存
return true;
}
完全二叉树满足:前 H-1 层是满的,但是最后一层不满。

利用层序遍历检查节点连续性
完全二叉树的层序遍历有一个关键特性:一旦在遍历中遇到第一个空节点,那么后续所有节点必须都是空节点(否则就不是完全二叉树)。
所以:
1、层序遍历走,空也进队列
2、遇到第一个空节点时,开始判断,后面全空就是完全二叉树,后面有非空就不是完全二叉树