前言:
相较于用顺序结构实现二叉树而言,用链式结构实现二叉树在编程上更为容易,但其方法往往难以想到,因为用到的是递归的思想。
因此此篇中包含大量有关递归函数推导过程的分析。
此外用链式结构实现二叉树,二叉树不一定是完全二叉树。
链式二叉树的创建:
1、头文件代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int TreeDataType;
typedef struct BinaryTreeNode
{
TreeDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BuynewNode(TreeDataType x);
2、测试文件代码:
#define _CRT_SECURE_NO_WARNINGS
#include "tree.h"
void TreeTest()
{
BTNode* node1 = BuynewNode(1);
BTNode* node2 = BuynewNode(2);
BTNode* node3 = BuynewNode(3);
BTNode* node4 = BuynewNode(4);
//BTNode* node5 = BuynewNode(5);
//BTNode* node6 = BuynewNode(6);
node1->left = node2;
node2->left = node4;
node1->right = node3;
//node2->right = node5;
//node3->right = node6;
}
int main()
{
TreeTest();
return 0;
}
3、申请新节点自定义函数代码:
BTNode* BuynewNode(TreeDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
assert(newnode);
newnode->data = x;
newnode->left = newnode->right = NULL;
return newnode;
}
4、二叉树结构图:
通过上述代码,建立了一个如下图的链式二叉树结构:
下图用于二次验证
有关链式二叉树自定义函数的实现:
1、头文件声明:
// ⼆叉树结点个数
int BinaryTreeSize(BTNode* root);
// ⼆叉树叶⼦结点个数
int BinaryTreeLeafSize(BTNode* root);
// ⼆叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
//⼆叉树的深度/⾼度
int BinaryTreeDepth(BTNode* root);
// ⼆叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, TreeDataType x);
// ⼆叉树销毁
void BinaryTreeDestory(BTNode** root);
2、求二叉树结点个数
代码:
int BinaryTreeSize(BTNode* root)
{
if (NULL == root)
{
return 0;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
递归过程:
注:红线——调用过程;蓝线——返回过程
分析:当第一次进入函数体时,root 非空 ;调用 return 语句中的 BinaryTreeSize 函数;循环调用,直至节点来到 4 位置处,此时再次调用 BinaryTreeSize 函数时,满足函数中的 if 条件,因此返回 0,再次回到 4 位置处,此时调用同行语句中的第二个 BinaryTreeSize 函数,同样满足 if 条件,返回 0 ,此时 节点 4 位置处的 return 语句后的值为 1+0+0 = 1,因此返回 1 给 节点3 ,
此时 节点3 第一个 BinaryTreeSize函数 的值已知,开始调用第二个 BinaryTreeSize函数 ,而 节点3 的右节点为 0 ,因此返回值为 0 ,此时 节点3 的return 行语句的返回值已知为:1+1+0 = 2
将 2 返回给 节点 1 ,此时 节点1 中的return行第一个 BinaryTreeSize函数 值已知,接下来执行第二个 BinaryTreeSize函数 ,同理可以得到他的返回值应为 1 ,最终递归函数回到 节点1 的位置处,此时 return 的返回值为 1+2+1 = 4,即为链式二叉树的节点总个数
结果验证:
二次验证:
3、求二叉树叶子节点个数
叶子节点的定义:没有左右子节点的节点被称为叶子节点
代码:
int BinaryTreeLeafSize(BTNode* root)
{
if (NULL == root)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
递归过程:
分析:
进入自定函数主体后,因为不满足 if 语句,会一直递归到 节点4 位置处,此时满足第二个 if 语句 返回1,此时指向 节点2 位置
节点2 的右节点为 NULL,因此再次返回1,此时指向 节点1 的位置,return行中,左节点 已遍历完,值为 1,此时调用第二个函数,同理能够得到返回值应为 1,且再次指向 节点1 的位置处,
此时 节点1 return行的返回值为 1+1 = 2;最后将2传递到主函数当中。
结果验证:
二次验证:
4、二叉树第k层节点个数
代码:
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (NULL == root)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
递归过程:
进入函数后,一路递归至节点4处,此时满足第二个 if 条件;返回1,此时指向 节点2
节点2 的右节点为NULL,满足第一个 if 条件,返回0,此时指向 节点1 ,开始调用return行 第二个函数,同理可得返回值应为 0 ,因此最终返回给主函数的值应当为 1;
结果验证:
二次验证:
5、求二叉树深度/高度
代码:
int BinaryTreeDepth(BTNode* root)
{
if (NULL == root)
{
return 0;
}
int leftdep = BinaryTreeDepth(root->left);
int rightdep = BinaryTreeDepth(root->right);
return leftdep >= rightdep ? 1 + BinaryTreeDepth(root->left) : 1 + BinaryTreeDepth(root->right);
}
递归过程:
分析:此处重点是比较根节点左右子节点的深度,而不是求和,使用三目操作符来比较左右子节点的深度,来确定最终的返回值
当进入函数时,会递归直至 节点4 ,此时再次递归函数时,会满足第一个 if 条件,返回0, 节点4 的 左节点个数 = 右节点个数 ,返回1,此时指向 节点2 ,开始统计 节点2 右节点个数,根据上图分析,个数为0 因此 节点2 的 左节点个数 > 有节点个数 ,返回 1 + 1 = 2,此时指向 节点1 的位置处,同理分析,可以得知 节点1 的右节点深度为 1,因此有 左节点个数 > 有节点个数,最终返回到主函数的值为 1+2 = 3
结果验证:
我们更改节点指向,使其深度为4,二次验证结果如下:
6、二叉树查找值
代码:
BTNode* BinaryTreeFind(BTNode* root, TreeDataType x)
{
if (NULL == root)
{
return NULL;
}
if (x == root->data)
{
return root;
}
BTNode* leftfind = BinaryTreeFind(root->left, x);
if(leftfind)
{
return leftfind;
}
BTNode* rightfind = BinaryTreeFind(root->right, x);
if (rightfind)
{
return rightfind;
}
return NULL;
}
递归分析:
分析:假设我们要查找4
当进入函数时,均不满足 if 条件,直至递归至 节点4 ,满足第二个 if 条件 ,并返回当前节点的地址,此时指向 节点2 的 leftfind 行处,并继续向下执行代码,此时对返回的 leftfind 进行判断,若非空说明接收到了正确的地址,直接返回,此时指向 节点1,同刚才的分析过程,因为 leftfind 接收到的返回值非空,会直接进入第三个 if 条件语句,并返回 leftfind 的地址,最终实现将 对应值的地址返回到主函数中,若在根节点的左子树上没有找到对应值,那么重复刚才的思想去右子树上找,若左右子树均为找到,则返回NULL;
结果验证:
二次验证:
7、链式二叉树的销毁
代码:
void BinaryTreeDestory(BTNode** root)
{
if (NULL == *root)
{
return ;
}
BinaryTreeDestory(&((*root)->left));
BinaryTreeDestory(&((*root)->right));
free(*root);
*root = NULL;
}
递归分析:
目的是找到每一个叶子节点,并将其释放并置空。
进入函数时,会递归至 节点4 的位置,再次递归时,满足 if 条件并返回,此时程序继续递归第二个函数,同样满足 if 条件 返回,此时 节点4 的左右子节点均为 NULL ,释放 节点4 并 置空,此时返回 节点2 位置处,重复刚才的操作,直至链表二叉树中所有的节点释放完毕。
结果验证:
销毁前:
销毁后:
链式二叉树的层序遍历
层序遍历不在使用递归的思想,而是通过队列来实现该函数。需要调用先前写过的队列函数,并将其头文件部分修改。修改代码如下:
typedef struct BTNode* QDatatype;//先前节点中存放的是int型数据,现将 存放的是二叉树节点的指针
typedef struct QueueNode
{
QDatatype data;
struct QueueNode* next;
}QueueNode;
typedef struct Queue
{
QueueNode* phead;
QueueNode* ptail;
int size;
}Queue;
代码:
void BinaryLevelOrder(BTNode* root)
{
Queue p;
InitQueue(&p);
QueuePush(&p, root);
while (!BoolEmpty(&p))
{
BTNode* front = QueueFront(&p);
printf("%d->", front->data);
QueuePop(&p);
if (front->left)
{
QueuePush(&p, front->left);
}
if (front->right)
{
QueuePush(&p, front->right);
}
}
}
思路:
目的是将链表二叉树一层一层的输出,我们通过队列,先将根节点入队,此时进入循环。
分析循环过程:
用 front 接收队头数据(二叉树节点指针),并打印,打印完毕后,pop队头数据,此时队列为空,但通过 front 依然能够访问二叉树中的左右非空子节点,此时将左右子节点依次入队,第一次循环结束。
当队列非空时,进入第二次循环,再次用 front 接收队头数据(二叉树节点指针),此时队头存放的地址已发生改变,(存放的是链表中节点2的地址),循环上述操作,能够把节点2的数据打印并出队列,同时再将节点2对应的左右非空子节点入队,如此循环直至队列为空
结果验证:
二次验证:
判断是否为完全二叉树
思路:
用链式结构实现二叉树时,会有一个明显区别,即:
完全二叉树,当非空数据全部出队时,此时队列中只有NULL数据;
而非完全二叉树,循环出队时(以遇到第一个NULL为循环停止条件),此时队列中还有非空的数据
注:在分析代码前,需要弄清楚一点:存放的数据为NULL ≠ 队列为空
上述分析过程中,可以确定一点,此代码需要将NULL数据一并存放到队列中,作为判定条件,先前说过,队列中存放的是 指针数据 , 那么对于空指针NULL而言,其本身也是数据之一,所以即使队列中全为NULL,那么该队列也为非空。
而队列中的bool判定是针对队列是否为空做出判断的,当把 NULL 数据入队时,队列非空,只不过存储的都是 NULL。
代码:
bool BinaryTreeComplete(BTNode* root)
{
Queue p;
InitQueue(&p);
QueuePush(&p, root);
while (!BoolEmpty(&p))
{
BTNode* front = QueueFront(&p);
QueuePop(&p);
if (front == NULL)//当取到第一个NULL数据时,结束循环
{
break;
}
QueuePush(&p, front->left);
QueuePush(&p, front->right);
}
while (!BoolEmpty(&p))//把队列中剩下的数据全部出队
{
BTNode* front = QueueFront(&p);
if (front != NULL)//若有非空数据,表明不是完全二叉树
{
QueueDestroy(&p);
return false;
}
QueuePop(&p);
}
QueueDestroy(&p);//循环结束时,表面为完全二叉树
return true;
}
结果验证:
改变节点连接方式,二次验证: