二叉树
二叉树链式结构的实现
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
//二叉树结构
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType data;
struct BinaryTreeNode* left_child;
struct BinaryTreeNode* right_child;
}TreeNode;
//创建节点
TreeNode* BuyNode(BTDataType x) {
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
assert(node);
node->data = x;
node->left_child = NULL;
node->right_child = NULL;
return node;
}
//创建二叉树
TreeNode* CreateTree() {
TreeNode* node1 = BuyNode(1);
TreeNode* node2 = BuyNode(2);
TreeNode* node3 = BuyNode(3);
TreeNode* node4 = BuyNode(4);
TreeNode* node5 = BuyNode(5);
TreeNode* node6 = BuyNode(6);
node1->left_child = node2;
node1->right_child = node4;
node2->left_child = node3;
node4->left_child = node5;
node4->right_child = node6;
return node1;
}
创建出来的二叉树如图所示,顺便让大家再记起一下之前提起过的二叉树的结构:根,左子树,右子树
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
先序 中序 后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。(根左右)
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。(左根右)
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。(左右根)
接下来我们以这副图为例子 先后进行遍历的结果分析
先序:根左右,所以从根开始,1
再向左子树出发,2,3,N,N,N
右子树,4,5,N,N,6,N,N
最后结果为:1 2 3 N N N 4 5 N N 6 N N
同理,中序:N 3 N 2 N 1 N 5 N 4 N 6 N
后序:N N 3 N 2 N N 5 N N 6 4 1
如果按照结构来划分,还可以写成这种样子:
而写成代码该如何写呢?那么首先我先把代码展示给大家
void PreOrder(TreeNode* root) {
if (root == NULL){
printf("N ");
return;
}
printf("%d ", root->data);
PreOrder(root->left_child);
PreOrder(root->right_child);
}
void InOrder(TreeNode* root) {
if (root == NULL) {
printf("N ");
return;
}
InOrder(root->left_child);
printf("%d ", root->data);
InOrder(root->right_child);
}
void PostOrder(TreeNode* root) {
if (root == NULL) {
printf("N ");
return;
}
PostOrder(root->left_child);
PostOrder(root->right_child);
printf("%d ", root->data);
}
接着我们以先序遍历为例子 给大家讲解一下这个过程
首先root为1,那么输出1,然后左节点递归到2;输出2,然后递归左节点到3;输出3,然后递归左节点为N输出N返回结束递归,再递归右节点为N输出N返回结束递归
于是开始返回到2,2的右节点递归为N返回;返回到1,1开始递归右节点4,输出4,然后递归4的左节点5,左节点5递归左节点右节点都为空,均输出N并返回到4,而递归右节点6也是一样,最后又把结果返回到1并结束进程
可以看出,在弄懂遍历过程之后,逐步递归的过程还是很简单的
二叉树的其他简单应用
求二叉树节点个数
先给大家看一个反面例子:
int TreeSize(TreeNode* root){
int size=0;
if(root==NULL){
return 0;
++size;
TreeSize(root->left_child);
TreeSize(root->right_child);
return size;
大家可能一般都会想到用这个size来记录节点的个数,可是如果这么使用,那么每个节点的递归中都会有一个属于自己的size,那么size的值是不固定的,我们不知道到底最后取到的是哪个size,如果以上面的图为例,那么会返回1,很明显,这是错的
有人会想说,可以使用static让size成为一个静态变量,那样的话就不会因为递归而造成size的值分为好几个,而是只在一开始初始化为0一次。但是这也是有问题的,如果我们重复使用,size的值就会叠加,从第二次开始就会不一样。但是这是可以解决的,我们可以让size不再是一个局部变量,而成为一个静态全局变量,在一开始时就定义,这样我们就可以在main函数中每次使用完之后将其重置为0,但这样也很麻烦,所以我们最后选择更精妙的方法!请看!
int TreeSize(TreeNode* root) {
return root == NULL ? 0 :
TreeSize(root->left_child) +
TreeSize(root->right_child) + 1;
}
这是一个使用三目运算符所构成的等式,其意思就是如果这个节点为空,那么返回0,不为空的话就返回这个节点的左子树和右子树的节点之和加1(这个节点本身)
还是以上面的图为例子举例,1不为空,+1,左子树2也不为空,+1,左子树3也不为空,+1,但3的左与右都为空,故3返回2的值为1,而2返回1的值为1+1=2,轮到右子树4不为空,+1,左子树5不为空,+1,但左子树的左与右为空,故返回4的值为1,而6同理也返回1,然后4返回1的值为1+1+1=3,最后返回到1节点的值就是2+3+1=6
求叶子节点个数
根据上面的求二叉树节点个数,大家肯定会想照葫芦画瓢,模仿其写一个,比如下面这样子:
int TreeLeafSize(TreeNode* root){
return !root->left_child && !root->right_child ? 1:
TreeLeafSize(root->left_child)+
TreeLeafSize(root->right_child);
}
这个逻辑确实很正确,左右都无子树时,此节点才为叶子节点,不然就是其左子树的叶子节点加上右子树的叶子节点数
但是,它忽略了一个问题,如果这个树是空树,又或者说,这个节点是空节点,它并没有给出判断的条件,这会导致程序崩溃的
当上面的二叉树遍历到2的时候,就会需要确认左子树和右子树的叶子节点,但是我们发现,2的右子树为空,那么这样就无法进行判断,它将尝试访问 root->left_child 或 root->right_child,这会导致未定义的行为,通常是程序崩溃。
正确代码如下:
int TreeLeafSize(TreeNode* root) {
//空节点
if (root == NULL)
return 0;
//叶子节点
if (!root->left_child && !root->right_child)
return 1;
//既不是空节点,也不是叶子节点
return TreeLeafSize(root->left_child)
+ TreeLeafSize(root->right_child);
}
有时候不用为了贪小便宜而失去代码的真正效用,多写一点也无所谓
求二叉树的高度
首先我们分治一下问题:
- 空 返回0
- 不是空 左子树高度和右子树高度 大的那个+1
那么这个时候又会有同学想到三目操作符,写出一段很简洁,运行起来却很麻烦的代码
int TreeHeight(TreeNode* root){
return root==NULL ? 0 :
TreeHeight(root->left_child) > TreeHeight(root->right_child)?
TreeHeight(root->left_child)+1:TreeHeight(root->right_child)+1;
看上去的确清晰明了,但里面有一个严重的问题影响了效率
那就是它会先比较谁大谁小,然后再返回这个高度值,但是问题在于,它一开始先比大小并没有记住数据,而只是比了比大小,这就导致在知道谁大谁小之后,我们依然需要再次向下递归,寻找深度,这是很浪费时间的
而正确方法如下,有两种方法可供选择:
//需要包含<math.h>头文件
int TreeHeight(TreeNode* root)
{
if (root == NULL)
return 0;
return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1;
}
int TreeHeight(TreeNode* root)
{
if (root == NULL)
return 0;
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
第k层的节点个数
首先分治子问题
- 空,返回0
- 不为空且k==1,返回1
- 不为空且大于1,返回左子树的k-1层与右子树的k-1层
代码如下:
int TreeLevelK(TreeNode* root,int k) {
assert(k > 0);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return TreeLevelK(root->left_child, k - 1) +
TreeLevelK(root->right_child, k - 1);
}
二叉树查找值为x的节点
首先,还是给大家先看一个大家很容易写出来的错误例子
TreeNode* TreeFind(TreeNode* root, BTDataType x) {
if (root == NULL)
return NULL;
if (root->data == x)
return root;
TreeFind(root->left_child, x);
TreeFind(root->right_child, x);
}
乍一看好像没问题,节点为空返回空,找到了就返回这个节点,这个不是就从左子树和右子树里面找,但实际上隐藏着一个问题,我们还是以上面的图为例,假设我们要找的节点值为3
首先从1开始,不为空也不是,找左节点;到2,也不是,找左节点;到三之后,是的,返回这个节点,可是我们并未设置一个变量来存储它的这个值,那么这个值就会丢失,如果我们使用这个函数寻找,它最后都会返回NULL
有些同学还会这样子写:
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
return TreeFind(root->left, x) || TreeFind(root->right, x);
}
虽然这个逻辑也是对的,在左子树和右子树中寻找,但是这个||符号使得这个表达式成为了一个逻辑表达式,输出的结果只有0和1,也就是真与假,如果不找到地址,而只是看有没有这个值的节点的话,完全可以这样做,但是也需要把返回值改成bool类型
而正确的写法应该是这样的:
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
TreeNode* ret1 = TreeFind(root->left, x);
if (ret1)
return ret1;
TreeNode* ret2 = TreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
以上图来解释一下:我们还是找3,首先从1开始,1的ret1记录2的递归结果,2的ret1记录3的递归结果;3是的,返回到2的ret1中,ret1为真,返回到1的ret1中,最后返回到main函数中并输出其地址,层层相扣。
以上就是二叉树的几种简单函数,希望大家看完后可以略懂一二!