目录
- 前言
- 1. 前序遍历
- 2. 中序遍历
- 3. 后续遍历
- 4. 二叉树结点的个数
- 5. 二叉树叶子结点个数
- 6. 二叉树的高度
- 7. 二叉树第K层结点的个数
- 8. 二叉树查找值为x的结点
- 全部代码
- 总结
正文开始
前言
本文旨在介绍二叉树的链式存储中一些函数的实现
博客主页: 酷酷学!!!
更多文章, 期待关注~
前置说明: 在学习二叉树的基本操作前, 需要先创建一棵二叉树, 然后才能学习相关的基本操作. 由于目前阶段对二叉树结构掌握不够深入, 为了降低大家学习成本, 此处手动快速创建一棵简单的二叉树, 快速进入二叉树操作学习, 等二叉树结构了解查差不多时, 我们反过头来再研究二叉树真正的创建方式.
typedef int BTDatatype;
typedef struct BTNode
{
BTDatatype data;
struct BTNode* left;
struct BTNode* right;
}BTNode;
BTNode* BuyNode(int x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
BTNode* CreateTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
注意: 上述代码并不是创建二叉树的方式, 真正创建二叉树方式期待后续博客
首先实现下面的方法之前我们先来回顾一下二叉树:
什么是二叉树?
- 空树
- 非空:根结点,根结点的左子树、根结点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
1. 前序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个结点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
我们先来看前序遍历:
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。简单来说遍历的顺序为 根 -> 左子树 ->右子树
递归的本质: 拆成当前问题和子问题, 返回条件: 最小规模的子问题
我们可以把大问题化小, 首先拆成根, 左子树, 右子树的结构, 然后左子树 又可以拆成 根 左子树 右子树的结构, 结束条件为树为空树.
代码实现:
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
运行结果:
前序遍历递归图解:
如图我们可以看出:
前序遍历依次访问的数据为:
1 2 3 4 5 6
如果加上对空树的访问, NULL简写为N:
1 2 3 N N N 4 5 N N 6 N N
下面为递归展开图, 不太理解的伙伴可以参考:
2. 中序遍历
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
即, 每棵树 先访问左子树, 在访问根, 最后访问右子树
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
递归展开图:
3. 后续遍历
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
即, 每一棵树,先访问左子树, 在访问右子树, 最后访问根
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
4. 二叉树结点的个数
求二叉树结点的个数, 这里初学者很容易先到使用遍历来求结点个数,如下:
// 错误示范
int TreeSize(BTNode* root)
{
static int size = 0;
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
首先做个铺垫再来讲解这段代码的问题
静态全局变量和静态局部变量的区别在于作用域和生存周期。
静态全局变量: 静态全局变量在整个文件中都是可见的,即其作用域为整个文件。静态全局变量只能被当前文件内的函数访问,其他文件无法访问。静态全局变量的生存周期为整个程序的执行期间,即在程序启动时分配内存,在程序结束时释放内存。
静态局部变量: 静态局部变量只在定义它的函数内部可见,即其作用域为定义它的函数内部。静态局部变量的生存周期为整个程序的执行期间,即在程序启动时分配内存,在程序结束时释放内存。与普通局部变量不同的是,静态局部变量只会在第一次进入函数时初始化一次,之后每次进入函数都会保留上一次的值。
静态全局变量和静态局部变量都可以被修改,但是有一些区别:
静态全局变量: 静态全局变量可以被当前文件内的任何函数修改,因为其作用域为整个文件。其他文件无法直接修改静态全局变量。但是,由于其作用域广泛,可能会被不同函数多次修改,导致程序的可维护性降低。
静态局部变量: 静态局部变量只能在定义它的函数内部被修改,其他函数无法直接修改静态局部变量。由于其作用域限制在函数内部,静态局部变量对于其他函数来说是不可见的,因此可以更好地控制变量的访问权限。此外,静态局部变量在函数调用之间保持其值不变,可以用于在函数调用之间保持状态或者记录某些信息。
因为每次调用函数size都是在栈区开辟的局部变量, 当函数结束后局部变量也会被系统回收, 这里用了static来修饰size, 看似没什么问题, 但是如果多次调用函数问题就来了
那么如何解决, 我们可以使用全局变量:
int size = 0;
int TreeSize(BTNode* root)
{
if (root == NULL)
return 0;
else
++size;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
void TreeSize(BTNode* root, int* psize)
{
if (root == NULL)
return 0;
else
++(*psize);
TreeSize(root->left, psize);
TreeSize(root->right, psize);
}
但是上述的写法虽然可以, 但是每次调用之前都需要重置size的值,过于麻烦
int size = 0;
TreeSize(root, &size);
printf("TreeSize:%d\n",size);
size = 0;
TreeSize(root, &size);
printf("TreeSize:%d\n", size);
最好的解决方案还是回归本质, 因为二叉树本身就是递归定义的, 我们采用递归的思想, 分而治之, 问题就迎刃而解
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left)
+ BinaryTreeSize(root->right) + 1;
}
首先分为左子树和右子树, 每一棵树都有左子树和右子树, 我们只需要统计每一棵树的左子树和右子树总结点个数加上自己.为空就等于0, 不为空左子树结点个数加上右子树结点个数, 返回给上一层
5. 二叉树叶子结点个数
叶子结点的个数, 空树就返回0, 递归的思想, 如果左子树和右子树都是空, 则就是叶子节点, 返回1, 统计左子树和右子树的叶子节点个数即可.
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);
}
6. 二叉树的高度
还是采用递归的思想, 一棵树的高度等于左子树的高度与右子树的高度较高的那个树加+1, 结束条件空树就是0.
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int left = TreeHeight(root->left);
int right = TreeHeight(root->right);
return left > right ? left + 1 : right + 1;
}
7. 二叉树第K层结点的个数
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);
将复杂度问题划分为子问题, 每一层即下一层的k-1层结点的个数, 如果k==1则返回1.
8. 二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDatatype x)
{
if (root == NULL)
{
return NULL;
}
if (root == x)
{
return root;
}
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL;
}
这里要返回一个结点, 因为是递归调用函数, 所以返回值会逐层返回, 将树划分为子问题, 现在如果为NULL,则返回NULL, 如果找到了就返回该结点, 没找到就调用函数去查找左子树与右子树, 如果左子树找到了就返回左子树的结点不需要去右子树查找了, 如果都没找到则返回NULL.
全部代码
Tree.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int BTDatatype;
typedef struct BTNode
{
BTDatatype data;
struct BTNode* left;
struct BTNode* right;
}BTNode;
BTNode* BuyNode(int x);
void PrevOrder(BTNode* root);
void InOrder(BTNode* root);
void PostOrder(BTNode* root);
//二叉树结点的个数
int BinaryTreeSize(BTNode* root);
//二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树的高度
int TreeHeight(BTNode* root);
//二叉树第K层结点的个数
int BinaryTreeLevelKSize(BTNode* root ,int k);
//二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDatatype x);
Tree.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Tree.h"
BTNode* BuyNode(int x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
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);
}
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int left = TreeHeight(root->left);
int right = TreeHeight(root->right);
return left > right ? left + 1 : right + 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);
}
BTNode* BinaryTreeFind(BTNode* root, BTDatatype x)
{
if (root == NULL)
{
return NULL;
}
if (root == x)
{
return root;
}
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Tree.h"
BTNode* CreateTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
int main()
{
BTNode* root = CreateTree();
PrevOrder(root);
//InOrder(root);
//PostOrder(root);
//int size = BinaryTreeSize(root);
//int size = TreeHeight(root);
//int size = BinaryTreeLeafSize(root);
//int size = BinaryTreeLevelKSize(root,3);
//printf("%d ", size);
return 0;
}
总结
关于二叉树这块比较复杂, 递归的思路需要清晰, 每一次递归调用的作用是什么, 为什么要这样写, 写代码之前手动画一遍递归调用简图, 只要思路通了, 代码就顺了, 不要觉得是在浪费时间, 因为时间本就是来解决问题的.
完, 感谢关注~