二叉树链式结构的遍历实现

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;
}

注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。

再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:

  1. 空树

  2. 非空:根结点,根结点的左子树、根结点的右子树组成的。

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。

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 执行过程

  1. 类型定义阶段

    • 首先执行typedef int BTDataType,将int类型重命名为BTDataType
    • 接着定义struct BinaryTreeNode结构体,并通过typedef重命名为BT,完成二叉树节点的类型定义
  2. 调用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的地址
  3. 调用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

整个过程从类型定义开始,先创建节点并构建二叉树结构,最后通过递归实现前序遍历并输出结果。

2.4 逻辑过程(程序执行的逻辑流程)

  1. 类型定义与函数声明

    • 逻辑上先完成数据类型的抽象定义:通过typedefint定义为BTDataType(数据类型抽象),定义BinaryTreeNode结构体(描述二叉树节点的逻辑结构:包含数据域和左右孩子指针),并简化命名为BT
    • 声明BuyNodeCreatBinaryTreePrevOrder三个函数,确定函数的输入输出逻辑。
  2. 二叉树的构建逻辑

    • 节点创建BuyNode函数的逻辑是 "输入一个值→申请内存→初始化节点(数据赋值、指针置空)→返回节点",封装了单个节点的创建逻辑。
    • 树结构构建CreatBinaryTree函数通过逻辑步骤构建树:先创建 6 个独立节点(值 1-6),再通过指针赋值建立节点间的父子关系(如node1->left = node2表示 1 的左孩子是 2),最终形成一个固定结构的二叉树,逻辑上确定了树的拓扑关系。
  3. 前序遍历的逻辑流程

  • 遵循 "根→左→右" 的递归逻辑:
  • 若当前节点为NULL,逻辑上表示 "空节点",输出N
  • 否则先输出当前节点数据,再递归处理左子树(逻辑上的 "左"),最后递归处理右子树(逻辑上的 "右");
  • 通过递归调用,将整棵树的遍历拆解为单个节点的处理逻辑,最终按前序顺序输出所有节点(包括空节点)。

2.5 物理过程(程序在计算机中的实际执行操作)

  1. 编译与内存分配

    • 编译阶段:类型定义和函数声明被编译器解析,确定BT类型的内存大小(int数据 + 两个指针,通常为 16 字节,取决于系统)。
    • 运行时:
      • BuyNode函数调用malloc(sizeof(BT)),向操作系统申请一块连续的内存(物理地址上的一块空间),用于存储节点数据;
      • 若申请成功,通过指针node指向该内存块,然后将参数x写入数据域,将左右指针域物理地址设为0NULL的物理表示)。
  2. 二叉树的物理存储

    • 6 个节点通过BuyNode分别在内存中占据独立的物理块(地址不连续);
    • CreatBinaryTree中的指针赋值(如node1->left = node2),本质是将node2的物理内存地址写入node1的左指针域,通过物理地址的关联形成 "树结构"(逻辑上的父子关系对应物理上的地址指向)。
  3. 前序遍历的物理执行

    • 递归调用时,每次调用PrevOrder会在栈内存中创建函数栈帧(存储参数root的物理地址、返回地址等);
    • 访问root->data时,通过root存储的物理地址找到节点内存块,读取其中的数据域并输出;
    • 递归访问左 / 右子树时,实际是将左 / 右指针存储的物理地址作为新参数压入栈,重复上述过程;
    • rootNULL(物理地址0),输出N 并弹出当前栈帧,返回上一层调用。
  4. 最终结果的物理表现

    • 所有输出操作通过标准输出流(如控制台)将字符序列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
  • 计算根节点 1 的右子树(节点 3)的高度:
    • 节点 3 的左右子树都是NULL
    • 所以TreeHeight(节点3) = 1(自身 1 层 + 左右子树高度 0)

计算当前节点的高度

  • 逻辑:当前节点的高度 = 自身这一层(+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 层

所以:

总结

这三个函数的编写思路高度统一:

  1. 利用二叉树的递归结构,将整体问题拆解为 "根节点处理"+"左右子树递归处理";
  2. 明确空节点的边界条件,确保递归终止;
  3. 根据不同统计目标(节点总数 / 叶子数 / 高度),设计 "当前节点贡献值" 与 "子树结果" 的组合方式(累加 / 条件判断 / 取最大值)。

这种思路既符合二叉树的结构特性,又使代码简洁易懂,充分体现了递归在处理树结构问题时的优势。

运行一下:

注意:

不要

这样写,因为这段代码确实存在严重的效率问题,核心原因是对同一子树进行了冗余的递归计算,导致时间复杂度呈指数级增长,尤其在处理深度较大的树时,效率会极其低下。通过引入临时变量存储中间结果,可以彻底解决这个问题。

我们通过具体分析来理解:

问题根源:重复计算同一子树

假设我们要计算某个节点的高度,这段代码的执行逻辑是:

  1. 先调用TreeHeight(root->left)计算左子树高度(记为 A);
  2. 再调用TreeHeight(root->right)计算右子树高度(记为 B);
  3. 比较 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 次,效率差距悬殊。

所以用leftHeightrightHeight存储子树高度,避免重复调用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 层节点数

逻辑拆解

  1. 边界条件处理

    • 若当前节点为NULL(空树),则该子树不存在第 k 层节点,返回 0;
    • k == 1,说明当前节点就是第 k 层的节点(因为从根节点开始算第 1 层),返回 1。
  2. 递归分解

    对于非空节点且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 个。

分解过程:

  1. 观察第 3 层节点与上层的关系:

    • D 是 B 的左孩子(B 在第 2 层);
    • E 是 C 的左孩子,F 是 C 的右孩子(C 在第 2 层)。
      所以,整棵树的第 3 层节点 = B 的子树中的第 2 层节点 + C 的子树中的第 2 层节点
  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)。
  3. 结论:

    整棵树(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):

  1. TreeLevelKSize(A, 3)

    • A 非空,k≠1,所以计算左子树 B 的第 2 层(3-1=2) + 右子树 C 的第 2 层(3-1=2)。
  2. 计算TreeLevelKSize(B, 2)

    • B 非空,k=2≠1,计算 B 的左子树 D 的第 1 层(2-1=1) + B 的右子树(空)的第 1 层。
    • D 非空,k=1,返回 1;右子树为空,返回 0。总和 = 1+0=1。
  3. 计算TreeLevelKSize(C, 2)

    • C 非空,k=2≠1,计算 C 的左子树 E 的第 1 层 + C 的右子树 F 的第 1 层。
    • E 非空,k=1,返回 1;F 非空,k=1,返回 1。总和 = 1+1=2。
  4. 最终结果: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的节点并返回该节点的地址,采用递归方式实现了深度优先搜索(先检查当前节点,再遍历左子树,最后遍历右子树),具体逻辑如下:

遵循 "先根后左右" 的查找顺序:

  1. 先检查当前节点是否为目标节点;
  2. 若不是,递归查找左子树;
  3. 若左子树中找到,直接返回结果;
  4. 若左子树中未找到,再递归查找右子树;
  5. 若左右子树都未找到,返回NULL

逻辑拆解

  1. 边界条件处理

    cpp 复制代码
    if (root == NULL)
        return NULL;

    若当前节点为空(递归到空树),说明该路径上没有目标节点,返回NULL

  2. 检查当前节点

    cs 复制代码
    if (root->data == x)
        return root;

    若当前节点的值等于x,直接返回当前节点的地址(找到目标)。

  3. 递归查找左子树

    cpp 复制代码
    BT* ret1 = TreeFind(root->left, x);
    if (ret1)
        return ret1;

    先在左子树中查找,用ret1存储查找结果;

    • ret1不为NULL(左子树中找到目标节点),直接返回该结果(无需再查右子树)。
  4. 递归查找右子树

    cpp 复制代码
    BT* ret2 = TreeFind(root->right, x);
    if (ret2)
        return ret2;

    若左子树中未找到(ret1NULL),再在右子树中查找,用ret2存储结果;
    ret2不为NULL(右子树中找到目标节点),返回该结果。

  5. 未找到的情况

    cpp 复制代码
    return NULL;

    若当前节点、左子树、右子树中都没有值为x的节点,返回NULL

示例说明

以如下二叉树为例,查找值为5的节点:

查找过程:

  1. 从根节点 1 开始,1≠5,递归查找左子树(节点 2)。
  2. 节点 2 的值≠5,递归查找其左子树(节点 3)。
  3. 节点 3 的值≠5,其左右子树为空,返回NULL(左子树未找到)。
  4. 回到节点 2,右子树为空,返回NULL(左子树整体未找到)。
  5. 回到根节点 1,递归查找右子树(节点 4)。
  6. 节点 4 的值≠5,递归查找其左子树(节点 5)。
  7. 节点 5 的值=5,返回节点 5 的地址。
  8. 最终结果:成功返回值为 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、遇到第一个空节点时,开始判断,后面全空就是完全二叉树,后面有非空就不是完全二叉树

相关推荐
码破苍穹ovo6 小时前
堆----1.数组中的第K个最大元素
java·数据结构·算法·排序算法
lifallen8 小时前
深入解析RocksDB的MVCC和LSM Tree level
大数据·数据结构·数据库·c++·lsm-tree·lsm tree
kokunka11 小时前
【数据结构】栈的顺序存储(整型栈、字符栈)
数据结构
小指纹12 小时前
图论-最短路Dijkstra算法
数据结构·c++·算法·深度优先·图论
再卷也是菜15 小时前
数据结构(12)二叉树
数据结构
qq_5139704415 小时前
力扣 hot100 Day63
数据结构·算法·leetcode
lifallen15 小时前
AbstractExecutorService:Java并发核心模板解析
java·开发语言·数据结构·算法
神器阿龙16 小时前
排序算法-归并排序
数据结构·算法·排序算法
爱吃KFC的大肥羊18 小时前
C/C++常用字符串函数
c语言·数据结构·c++·算法