我们之前学习了树的遍历,体会到了递归的暴力美学。现在我们来实现剩下的二叉树操作,关于二叉树,我们知道一般有不止一个叶子结点,所以这里不讲删除和插入结点的操作,因为可以插入和删除结点的位置太多了,没有什么意义,后面讲红黑树的时候会学到。
头文件声明
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
typedef char BTDataType;//类型重命名
//二叉树结点的结构
typedef struct BinaryTreeNode
{
BTDataType data;//数据域
struct BinaryTreeNode* left;//指向左孩子的指针
struct BinaryTreeNode* right;//指向右孩子的指针
}BTNode;
//先序遍历
void PreOrder(BTNode* root);
//中序遍历
void InOrder(BTNode* root);
//后序遍历
void PostOrder(BTNode* root);
//二叉树结点个数
int BinaryTreeSize(BTNode* root);
//叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树高度
int BinaryTreeLevel(BTNode* root);
//二叉树第k层结点个数
int BinaryTreeLevelkSize(BTNode* root, int k);
//查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
//销毁二叉树
void BinaryTreeDestroy(BTNode** root);
//层序遍历
void LevelOrder(BTNode* root);
//判断是否为完全二叉树
bool CompleteBinaryTree(BTNode* root);
我们每个函数实现后都用之前创建好的二叉树来进行简单功能测试:

同时因为之前在讲树的遍历的时候有说明过详细递归过程,这里不再赘述,可以参考我之前发布的博客:数据结构:树的遍历。
二叉树结点个数
二叉树的总结点个数=根结点+左子树结点个数+右子树结点个数。对于递归式结构的二叉树来说,我们就可以去递归遍历它的左右子树,如果结点不存在则返回0。
代码来说就很简单了:
cpp
//二叉树结点个数
int BinaryTreeSize(BTNode* root)
{
//如果结点不存在则直接返回0
if (root == NULL)
{
return 0;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
往下递归它的左右子树,并返回结点个数,这个代码就完成了。我们测试一下:

结果与预期符合,说明代码逻辑没什么问题。
二叉树叶子结点个数
我们知道,叶子结点就是度为0(也可以说没有左右孩子结点)的结点。
叶子结点的个数=左子树叶子结点个数+右子树叶子结点个数。
所以我们还是左右子树往下递归去查找,我们传进去一个根结点,保证它存在,如果它的左右结点都不存在(为NULL),那么就说明这个结点是一个叶子结点,返回1;如果根结点不存在,那就直接返回0,没有查找的必要了。
函数代码如下:
cpp
//叶子结点个数
int BinaryTreeLeafSize(BTNode* root)
{
//根结点不存在,无需往下查找
if (root == NULL)
return 0;
//根结点存在,判断是否为叶子结点
if (root && root->left == NULL && root->right == NULL)
return 1;
//返回左右子树叶子结点之和
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
我们来测试一下:

结果符合预期,代码逻辑没有什么问题。
二叉树的高度
二叉树的高度(深度),实际上就是根结点+左右子树(高的那个)的高度。
按图来理解就是这样的:

因为要比较左右子树的高度,所以我们需要两个变量left_level和right_level来存储左右子树的高度,然后max来存储高度比较高那个值,和根结点相加作为二叉树的高度返回来。
同样的,如果根结点不存在,我们则返回0。
代码如下:
cpp
//二叉树高度
int BinaryTreeLevel(BTNode* root)
{
if (root == NULL)
return 0;
//存储左右子树高度
int left_level = BinaryTreeLevel(root->left);
int right_level = BinaryTreeLevel(root->right);
//比较哪个高度高
int max = left_level > right_level ? left_level : right_level;
//返回二叉树高度
return 1 + max;
}
我们简单测试一下:

结果符合预期,代码逻辑没什么问题。
二叉树第k层的结点个数
二叉树第k层结点个数可以这样找,因为根结点是第一层,所以第k层的结点个数就是根结点的左右子树第k-1层的结点个数,左右子树一直往下递归,直到变成左右子树第1层的结点个数,就可以找到第k层的结点个数了。

如果是第一层,那么就是根结点,返回1,如果根结点不存在,直接返回0。
代码如下:
c
//二叉树第k层结点个数
int BinaryTreeLevelkSize(BTNode* root, int k)
{
//边界防护
if(k<=0)
return 0;
if (k == 1 && root)
return 1;
if (root==NULL)
return 0;
return BinaryTreeLevelkSize(root->left, k-1)
+ BinaryTreeLevelkSize(root->right, k-1);
}
我们简单测试一下:

结果符合预期,代码逻辑没什么问题。
查找值为x的结点
查找这个结点,先从根结点开始,如果根结点不是则往左右子树开始遍历,如果左子树中找到了则不用遍历右子树,返回该结点指针;没找到则遍历右子树看是否能找到,都找不到则说明该结点不存在,返回NULL。
如果根结点为NULL,则不用查找了,直接返回NULL。
代码如下:
c
//查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root->data == x)
return root;
if (root == NULL)
return NULL;
//根结点不是,从左右子树中去找
BTNode* LeftFind = BinaryTreeFind(root->left, x);
//找到则返回该结点,停止从右子树中去查找
if (LeftFind)
return LeftFind;
BTNode* RightFind = BinaryTreeFind(root->right, x);
if (RightFind)
return RightFind;
//都找不到则返回NULL
return NULL;
}
我们简单测试一下:

结果符合预期,说明代码没什么问题。
二叉树的销毁
因为我们的二叉树是由一个个结点组成的,每个结点都是一个一级指针,要销毁二叉树,我们就需要传过去根结点的地址,也就是一个二级指针。(希望形参的改变可以影响实参,释放后要把root置为NULL)
如果我们先释放根结点的内存,那么就找不到它的左右子树并释放了,所以我们可以按左右根的顺序销毁二叉树,跟后序遍历一样。如果根结点为空,则说明二叉树为空,销毁完成,直接返回。
代码如下:
c
/销毁二叉树
void BinaryTreeDestroy(BTNode** root)
{
if (*root == NULL)
return;
//先释放左右子树
//注意传过来的是二级指针
BinaryTreeDestroy(&(*root)->left);
BinaryTreeDestroy(&(*root)->right);
//最后释放根结点
free(*root);
*root = NULL;
}
层序遍历
层序遍历就是按顺序遍历打印每层的结点。

如这棵二叉树,按层序遍历顺序就是:
A B C D NULL E F G NULL NULL NULL NULL NULL NULL NULL NULL(NULL也可以不打印)
那么如何来实现呢?按照层序遍历顺序来看,我们打印完根结点要拿到根结点的左孩子结点,打印完再拿到根结点的右孩子结点。打印完再拿到B的左孩子,然后右孩子,然后是C的左右孩子一直到遍历完整个二叉树。
要达成层序遍历,我们就需要借助数据结构:队来实现。
实现思路是这样的,我们先把根结点入队,然后拿到它的左右孩子,不为空则入队列。

然后我们取队头元素,打印队头元素,然后执行出队操作,把A出队。

然后取队头结点B,将B的左右孩子(不为空)入队。

再把B打印一下,然后执行出队操作:

然后再取队头,将队头的左右孩子入队列,再打印队头,然后出队,这样一直循环往复,直到队列为空,层序遍历就结束了。

我们把之前实现队列的接口函数的文件添加进去,将队列中存放的元素类型改为结点类型BTNode*,再包含一下头文件。该函数先定义一个队列,初始化和销毁中间写主要思路。
我们先把根结点入队,保证队列不为空(为空则不会执行下列循环),队列不为空则执行以下循环操作:取队头、入队头左右结点、打印队头、出队。直到队列为空时跳出循环,层序遍历结束。
代码实现如下:
c
//层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
//根结点入队
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
//取队头
BTNode* top = QueueHead(&q);
//入左右孩子结点
if (top->left)
QueuePush(&q, top->left);
if (top->right)
QueuePush(&q, top->right);
//打印队头然后出队
printf("%c ", top->data);
QueuePop(&q);
}
printf("\n");
QueueDestroy(&q);
}
我们简单测试一下:

结果符合预期,代码逻辑没什么问题。
判断是否为完全二叉树
我们知道,完全二叉树的结点和序号是一一对应 的关系,要判断是否是完全二叉树,同样地,我们也要借助队列来实现,但和层序遍历有所区分的是,不管左右结点是否为空,我们都要入队列。

比如说这棵二叉树,很明显不是一棵完全二叉树,我们来看入队列的过程:

先入根结点,然后我们入队头的左右结点,取出根结点:

继续入队头的左右结点,取出队头:



下一次取到的就是NULL了,此时我们结束第一次循环,进入第二次取队头循环:

我们会发现第二次循环中取到了不为空的结点。
接下来我们举个完全二叉树的例子:

第一次循环中入队头左右结点,出队头,一直到遇到结点为空的情况是这样的:

此时队头为空,跳出第一次循环,进入第二次循环取队头,发现全取出来了,没有遇到结点不为空的情况。
从以上两个例子中我们可以看出来,完全二叉树和非完全二叉树在第一次循环中没有差别,重点在于第二次循环,完全二叉树取队头不会遇到队头不为空的情况,而非完全二叉树会。
所以在第二次循环中,如果遇到top!=NULL的情况,则说明该树是非完全二叉树,返回false。第二次循环顺利结束,则说明是完全二叉树,返回true。
这两次循环条件都是队列不为空。
代码如下:
c
//判断是否为完全二叉树
bool CompleteBinaryTree(BTNode* root)
{
Queue q;
QueueInit(&q);
//根结点入队
QueuePush(&q, root);
//第一次循环
while (!QueueEmpty(&q))
{
//取队头
BTNode* top = QueueHead(&q);
//取到队头为空,跳出循环
if (top == NULL)
break;
//入左右孩子结点
QueuePush(&q, top->left);
QueuePush(&q, top->right);
QueuePop(&q);
}
while (!QueueEmpty(&q))
{
//取队头
BTNode* top = QueueHead(&q);
//取到队头不为空,说明是非完全二叉树
if (top != NULL)
return false;
QueuePop(&q);
}
QueueDestroy(&q);
}
我们简单测试一下:

结果符合预期,代码没什么问题。