目录
- 链式二叉树的结构及其声明
- 链式二叉树的四种遍历方式
- 前序遍历
- 中序遍历(中根遍历)
- 后序遍历
- 层序遍历
- 概念
- 思路分析
- 详细代码
- 求树的节点个数
- 变量累加法(错误)
- 分治递归法
- 求树的叶子节点个数
- 警惕空指针
- 正确代码
- 求第k层节点个树
- 思路分析及规则明细
- 代码详细
- 求树的深度/高度
- 规则明细及思路分析
- 错误代码示范
- 代码详细(正确)
- 查找指定节点
- 思路分析
- 代码详细
- 销毁二叉树
- 思路分析
- 代码详细
链式二叉树的结构及其声明
- 首先来看看它的结构声明。结构体中有三个成员,一个是当前结点的值,还有两个是指向当前结点左孩子结点的指针以及指向右孩子结点的指针
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType data;
struct BinaryTreeNode* lchild;
struct BinaryTreeNode* rchild;
}BTNode;
- 也就是下面这种样子
链式二叉树的四种遍历方式
前序遍历
- 规则:根——左子树——右子树
- 总的来说,我们必须把对于每个节点根节点(自己)访问完,然后访问他的左孩子节点,而对于左孩子节点他也必须访问根节点(自己),然后访问他的左孩子节点,指定为NULL,然后开始访问他的右孩子节点.
- 对于1这个节点,先访问本身,打印1;然后接着访问他的左孩子节点2,对于2也是访问他本身,打印2,然后访问他的左孩子节点4,对于4也是访问他本身,打印4,然后访问他的左孩子节点NULL,发现左孩子节点为NULL直接返回(也可以选择打印)
/*先序遍历*/
void PreOrder(BTNode* root)
{
if (!root)
{
//printf("NULL ");可以选择打印
return;
}
printf("%d ", root->data);
PreOrder(root->lchild);
PreOrder(root->rchild);
}
- 结果就是1 2 4 3
中序遍历(中根遍历)
- 规则:左子树——根——右子树
- 了解了前序遍历,那中序遍历也不下话下。和先序做一个区分
- 这不简单了吗?前面是先访问根节点,然后依次访问他的左孩子节点,然后左孩子节点又访问根节点(本身)然后又访问他的左还子节点
- 然后咱们就依葫芦画瓢,先访问左孩子节点,然后左孩子节点又访问他的左孩子节点,访问为空,然后访问根节点,然后根节点访问完,又访问右孩子节点
/*中序遍历*/
void InOrder(BTNode* root)
{
if (!root)
{
//printf("NULL ");
return;
}
PreOrder(root->lchild);
printf("%d ", root->data);
PreOrder(root->rchild);
}
- 以上一个二叉树1 2 3 4为例,他的中序遍历:4 2 1 3
后序遍历
规则:左子树——右子树——根
- 这里我相信大家都能自己推出来了,直接上代码吧。
/*后序遍历*/
void PostOrder(BTNode* root)
{
if (!root)
{
//printf("NULL ");
return;
}
PreOrder(root->lchild);
PreOrder(root->rchild);
printf("%d ", root->data);
}
- 1 2 3 4二叉树为例,结果: 4 2 3 1
层序遍历
概念
- 就是一层一层从左到右遍历打印,比如上面的二叉树,一层一层从左到右打印就是1 2 3 4 5
思路分析
- 这里层次遍历就无法使用递归了,递归要么是左子树递归到底,要么是右子树递归到底,我们这里必须把每层的左右都打印完
- 观察一下,也就是满足 1 2 3 4 5,按节点顺序来,我们前面说的数据结构队列的原则就是先进先出,是不是进满足这个规则。
下面讲讲具体步骤
- 第一步就是,把根节点入队列,此时这个节点是队头。(目的是让队列不为空)
- 第二步就是,取队头,并且打印数据,然后出队列(按照队头顺序打印)
- 第三步就是,如果队头的左右孩子不为空,将他们入队列(先进先出原则,把上一层的左右孩子打印)
- 第四步,从第二步开始重复,直到队列为空。
详细代码
void LevelOrdre(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while(!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
}
- 注意改变了了队列存储节点的数据类型为二叉树节点
- 入队列的节点注意是取出队头的左右孩子节点
求树的节点个数
变量累加法(错误)
- 有的同学看到,灵机一动,这么简单?直接定义一个局部变量累加就行了。想法挺好,但现实很残酷,我们每次调用函数都要开辟函数栈帧,而每个函数栈帧都要重新定义这个变量,变量每次都是从0开始。这时候结构每次就是1
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
int size = 0;
size++;
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
return size;
}
- 于是有同学呢又想出一种办法,就是将这个变量定义为静态变量,因为静态变量是存放在静态区中的,不会随着某一个函数的调用结束而销毁,因此我们可以这么写
int TreeNodeNum1(BTNode* root)
{
if (root == NULL)
return;
static int sz = 0;
sz++;
TreeNodeNum1(root->lchild);
TreeNodeNum1(root->rchild);
return sz;
}
- 可以看到,第一次的计算确实是可以计算出来这棵树有6个结点,但是在我多调用几次之后结点个数却发生了一个累加,这就是静态变量的特点,均会在上一次的基础上去进行一个运算,无论你是调用其他的什么函数或者是多调用几次,那其实可以看到这里就出现BUG了,
- 其实这一块很简单,我们不需要静态变量,直接将这个变量放到函数外部,作为一个全局变量即可,因为函数内部的返回值我们不好控制,干脆就不要返回值,直接将这个记数的变量定义为全局变量,这样就很好控制了
int sz = 0;
void TreeNodeNum1(BTNode* root)
{
if (root == NULL)
return;
sz++;
TreeNodeNum1(root->lchild);
TreeNodeNum1(root->rchild);
}
分治递归法
- 我们始终将一个树分割为三个部分,【根】【左子树】【右子树】,因此进行一个分块求解然后再加起来就可以了
- 也就是说我们把每一个子树的根节点和左孩子节点和右孩子节点的个数累加起来
- 注意的是,我们如果当前节点存在子树,子树应该至少有一个节点,所以统计每个子树节点个数的时候至少为1,如果节点为空,说明没有子树,直接返回0
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
求树的叶子节点个数
警惕空指针
- 求解树的叶子结点个数,叶子节点就是左孩子和右孩子节点都为NULL
- 前面说了如何去求一颗树的节点个数,那么求叶子节点个树就是不加上那些不是左右孩子都为NULL的节点
- 于是有同学就写了这样的代码,我们来看看有什么问题
int BinaryTreeLeafSize(BTNode* root)
{
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
- 所以我们要对当前节点是否为NULL进行判断,如果为NULL说明他肯定不是叶子节点
正确代码
- 根据上面的推测我们可以写出
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
求第k层节点个树
思路分析及规则明细
📚当k > 1 时,第k层的结点个数为第k - 1层左孩子节点个数 + k-1层右孩子的结点个数
📚当k == 1时,return 1
-
除了判断k之外,别忘了每次都要判断一下传进来的根结点是否为空,防止访问空指针。
-
,所以当k=1的时候,是第k层,第k层节点个数实际就是第k-1层的左右孩子节点个数,求k-1层节点的左右孩子节点即可
代码详细
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
求树的深度/高度
规则明细及思路分析
规则:二叉树高度 = 左子树和右子树中高度大的那个 + 1
- 思路就是我们需要比较二叉树的右子树和左子树的高度谁更高,此时就是整棵树的深度。想知道对于祖宗节点左子树的深度,就得求出祖宗节点左子树的左子树深度,依次类推比较返回。
- 值得注意的是,+1是因为如果二叉树不是空树,就至少高度为1(祖宗节点一层)
错误代码示范
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
return TreeHeight(root->lchild) > TreeHeight(root->rchild) ?
TreeHeight(root->lchild) + 1 : TreeHeight(root->rchild) + 1;
}
- 从运行结果可以看出是可以计算出来的,树的高度为4,但是呢却存在一个很大的隐患
这里我们直接画图
- 可以看到我们,如果每一次都不去存储左右子树的高度,他们比较完大小后需要重新递归去求左右子树的高度+1,也就造成了【重复计算】
- 所以我们说说下面正确代码
代码详细(正确)
int BinaryTreeDeapth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int LeftDepth = BinaryTreeDeapth(root->left);
int RightDepth = BinaryTreeDeapth(root->right);
return LeftDepth > RightDepth ? LeftDepth + 1 : RightDepth + 1;
}
- 这样把递归回调的结果存储起来,然后比较其左右子树的高度直接返回就可以避免重新递归。
查找指定节点
思路分析
- 想要在一个二叉树中查找指定的节点,无法就是在左右子树的节点里面去找。那么我们先查找左子树,如果找到了,就没必要去查找右子树。如果左右子树都查找完没有找到,则是没有该节点
代码详细
- 对于祖宗节点1的左右子树
- 强调对于2节点的左右子树
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* LeftNode = BinaryTreeFind(root->left, x);
if (LeftNode)
{
return LeftNode;
}
BTNode* RightNode = BinaryTreeFind(root->right, x);
if (RightNode)
{
return RightNode;
}
return NULL;
}
销毁二叉树
思路分析
- 同其他思路,如果像销毁整个二叉树,那么就得先销毁他的左右子树,那么对于左右子树,如果他们也有左右子树,需要先销毁他的左右子树,如果先销毁根节点,就无法找到他的左右子树了就是后序遍历)
代码详细
void BinaryDestroy(BTNode** root)
{
if (*root == NULL)
{
return;
}
BinaryDestroy(&((*root)->left));
BinaryDestroy(&((*root)->right));
free(*root);
*root = NULL;
}
- 这里二级指针是为了改变实参,如果需要接口统一,可以传一级指针,不过需要把实参手动设置NULL