二叉树是数据结构中一种相当重要的结构,对于日后的学习和我们对数据结构认识的提升都有着举足轻重的作用,今天我就带大家来一起学习一下二叉树。
目录
1、 二叉树节点个数 int BinaryTreeSize(BTNode* root);
2、 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root);
3、 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k);
4、 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
5、 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root);
6、 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root);
7、 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root);
8、 层序遍历 void BinaryTreeLevelOrder(BTNode* root);
10、 判断二叉树是否是完全二叉树 bool BinaryTreeComplete(BTNode* root);
BinaryTreeCreate(BTDataType* a, int* pi);
12、 二叉树销毁 void BinaryTreeDestory(BTNode* root);
首先学习一种数据结构,我们要首先认识一下对应数据结构的节点内容,对于二叉树来说,首先应该包含一下节点对应的数据本身,接着的话,二叉树应该有两个子树的地址,那一共应该是有三个元素,我们可以先把这一部分写出来。
typedef char Datatype;
typedef struct BinaryTreeNode
{
Datatype data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
按照学习的过程来说,我们应该先学习二叉树的增删查改,但因为二叉树底层是通过链表或顺序表实现,学习增删查改没有意义,在二叉树这样的数据结构里面,递归分制是一个非常重要的思想,具体表现在我们原本的一棵树,我们可以把它分为根,左树,右树,左树和右树又可以继续分为根,左树,右数,这里和数学里面的分形思想有着异曲同工之妙。所以对于二叉树的学习,我们一定会围绕递归展开。所以我们从别的方面来深入理解二叉树,具体体现为下面的这些方面:
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
下面我们会一个一个来学习它们。
1、 二叉树节点个数 int BinaryTreeSize(BTNode* root);
对于这个问题,我们可以通过递归分制的思想想到,我们先把树分为左树,右树,那二叉树的节点个数就是左树节点个数加上右树节点个数再加上1就是根,当节点为 NULL 时就是遍历结束,我们返回0,所以用代码标识出来就是
int BinaryTreeSize(BTNode* root)
{
if (!root)
return 0;
return 1 + BinaryTreeSize(root->left)
+ BinaryTreeSize(root->right);
}
2、 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root);
明白了上面的节点个数算法之后,这道题也就不难了,我们拿到一个根节点指针之后,我们先判断根节点地址是否为空,如果为空,那直接返回0,不为空的话我们再去想怎么判断叶子节点。叶子节点就是度为0的节点,既然度为0,那么左右子树必须都是空树。既然我们的根节点不为空,那么根节点本身必然不是叶子节点,那我们就要返回左右子树叶子节点的数量和。
int BinaryTreeLeafSize(BTNode* root)
{
if (!root)
return 0;
if ((!root->left) && (!root->right))
return 1;
return BinaryTreeLeafSize(root->left)
+ BinaryTreeLeafSize(root->right);
}
3、 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k);
我们拿到一个root指针之后,我们先判断地址是否是空,如果地址是空,那么不管是第多少层节点数量都是0,我们直接返回0,如果节点数量不为空,那么我们考虑K,如果 K==1 就是相当于是本层,那么就只有root对应的节点,所以返回1,其他情况下,假设我们传参时候传的(root,2);那我们就是要第二层的节点数量,那我们不就可以看作是左子树第一层和右子树第一层。所以我们可以递归左右子树的K-1层的节点数量,当K==1时候返回1,如果root为空返回0,即可。
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (!root)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
4、 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
查找二叉树值为x的节点,我们首先拿到root节点指针之后要先判断一下root是否为空,如果root为空,那我们肯定是找不到值为x的节点了,我们直接返回NULL就好了,如果root不为空,那么我们首先要判断root节点的值是否为x,如果是我们直接返回root节点就好了,如果不是的话,我们就要往左右子树找了。思路倒是不难,关键在于我们怎么返回我们找到的节点,第一次写这个接口的同学很可能会这样写
return BinaryTreeFind(root->left, x) || BinaryTreeFind(root->right, x);
我们的想法是先去找左树,找到了之后就返回root,就不用管右树了,但是这样的写法,它只能返回你有没有找到,不能返回找到的节点,因为这里用了布尔运算符返回的值也是布尔值,即真或者假。所以我们要达到想法的话,需要创建一个变量来接收我们找到的root。
BTNode* BinaryTreeFind(BTNode* root, Datatype x)
{
if (!root)
return NULL;
if (root->data == x)
return root;
BTNode* left = BinaryTreeFind(root->left, x);
if (left)
return left;
BTNode* right = BinaryTreeFind(root->right, x);
return right;
}
5、 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root);
要遍历一颗二叉树,我们可以依据分制的思想,先遍历根节点,然后左子树,右子树。这样的遍历我们就成为前序遍历,也叫先序遍历。代码实现也比较简单。
void BinaryTreePrevOrder(BTNode* root)
{
if (!root)
{
printf("#");
return;
}
printf("%d", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
6、 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root);
中序遍历,顾名思义就是我们对根节点的调用位于中序,具体体现就是左子树,根,右子树。
void BinaryTreeInOrder(BTNode* root)
{
if (!root)
{
printf("#");
return;
}
BinaryTreeInOrder(root->left);
printf("%d", root->data);
BinaryTreeInOrder(root->right);
}
7、 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root);
后序遍历就是最后调用根节点,具体顺序就是左子树,右子树,根。
void BinaryTreePostOrder(BTNode* root)
{
if (!root)
{
printf("#");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%d", root->data);
}
8、 层序遍历 void BinaryTreeLevelOrder(BTNode* root);
层序遍历不同于我们上面学习的三种遍历,层序遍历并非是递归分制的思想,而是一层一层的遍历。就像我们遍历一个数组一样,需要一个范围。二叉树本身的层序遍历中,范围并不好确定,所以我们考虑引入队列来辅助我们判断是否遍历完全,我们每次遍历掉一个节点之后我们就入队列它的子节点。
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if(root)
QueuePush(&q, root);
while(!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
if(front->left)
QueuePush(&q, front->left);
if(front->right)
QueuePush(&q, root->right);
QueuePop(&q);
}
QueueDestroy(&q);
}
10、 判断二叉树是否是完全二叉树 bool BinaryTreeComplete(BTNode* root);
判断二叉树是否为完全二叉树我们首先要知道完全二叉树的定义,前n-1层是满的,第n层的节点从左到右连续 。也就是说我们按照层序遍历的思想的话,完全二叉树遍历的结果在第一个空之前全都是非空,并且因为我们出队列一个节点,入队列它的左右子节点,所以当我们遍历到第一个空节点时候,队列中剩余的节点也不可能存在非空。
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if(root)
QueuePush(&q, root);
while(QueueFront(&q))
{
BTNode* front = QueueFront(&q);
QueuePush(&q, front->left);
QueuePush(&q, root->right);
QueuePop(&q);
}
while(!QueueEmpty(&q))
{
if(QueueFront(&q))
{
QueueDestroy(&q);
return false;
}
QueuePop(&q);
}
QueueDestroy(&q);
return true;
}
11、通过前序遍历数组来构建二叉树 BTNode*
BinaryTreeCreate(BTDataType* a, int* pi);
通过前序遍历创建二叉树,我们读数组,每次往后读一个,读到#就返回,但是这里有个问题,我们每次递归时候总不能下标从0开始吧,所以我们需要创建一个变量来记录我们下标所处位置。
BTNode* BinaryTreeCreate(Datatype* a, int* pi)
{
if(a[(*pi)++]=='#')
return NULL;
BTNode* root = malloc(sizeof(BTNode));
root->data = a[(*pi)++];
root->left = BinaryTreeCreate(a,pi);
root->right = BinaryTreeCreate(a,pi);
return root;
}
12、 二叉树销毁 void BinaryTreeDestory(BTNode* root);
二叉树的销毁就简单得多了,由于我们二叉树递归分制的思想,我们递归构建我们的二叉树,所以我们销毁时也要递归式的销毁,但是我们需要注意,我们销毁二叉树时只能用后序的顺序来销毁,因为如果采用前序或者中序的顺序来销毁的话,我们销毁了根节点后我们的子树就会找不到从而导致内存泄露。
void BinaryTreeDestory(BTNode* root)
{
if(root == NULL)
return;
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}