文章目录
☑️前言
🚩二叉树可以说是我们学习数据结构路上的第二个高的台阶,要想跨越它,需要我们多画图,多理解,多思考它的 递归 过程。前面 树的介绍 和 堆 这两篇文章让我们对树有了一定的了解,对于二叉树的概念也有说到,因此,本章不会对二叉树的概念及介绍深入去讲,而是着重于它的实现,着重于理解递归。
🚩二叉树是后面一些高阶数据结构的基础,例如:红黑树,AVL树,B树等等。并且,我们在面试当中,也经常会被面试官要求手撕二叉树的相关OJ
题,所以,二叉树这一章节就显得额外重要,可不敢遇到困难就退缩了。
🚩本章实现的二叉树是一棵普通的二叉树,与之前的数据结构有所不同,它的增删查改功能没那么有意义,那么什么时候有意义呢?我们在普通二叉树的基础上加上某一性质,使其能够通过这一性质来管理左右子树,这时候的增删查改就有意义了,例如搜索二叉树等等。搜索二叉树:
🚩所以本章的二叉树,是打基础。我们在学习的过程中,一定要细细品味整个递归的过程,最好就是画图理解。接下来就带大家实现一棵属于自己的初等二叉树吧!
关于二叉树的补充
在实现普通二叉树之前,我们还需知道一些东西。
- 对于 堆 ,是采用顺序结构来存储数据的,那是因为 堆 的逻辑结构是一棵完全二叉树,那么本章的二叉树是否也能采用顺序结构来存储数据呢?先来看下图:
可以看到,可以是可以,但是一棵二叉树不一定就是完全二叉树,如果采用顺序存储结构的话,这里可能会有很多的空间浪费,因此,二叉树将采用链式的储存结构来存储数据,每一个数据对应一个树结点(结构体)。由于是二叉,所以我们只需要定义两个树结点的指针分别指向该结点的左孩子和右孩子即可。
- 前面说了,二叉树是递归定义的,因此后面的功能接口的实现几乎全是采用递归方法。
- 对于一棵二叉树,我们可以把它分成根,左子树和右子树,而它的左子树和右子树又可以分为根,左子树和右子树,依次这样下去,整个二叉树就有了递归的性质了,所以为什么二叉树是递归定义的,这是很重要的原因。
- 对于二叉树的递归,一个很重要的思想 —— 分治思想 贯穿全文,它可以形象为: 一个大学的校长想统计整个学校的人数,然后这个校长就叫来
1
院长和2
院长去统计他们各自学院的人数,然后1
院长和2
院长又分别叫来各自的系主任去统计各自系的人数…最后分到每个寝室长统计各自寝室的人数,等最后一层统计完后,再依次上报,最终汇总到校长手中。这就是典型的分治思想。
得到一个树的结点
-
这里我们的普通二叉树实现都是在一个文件里:BinaryTree.c。
-
最开始我们要包含一下所需头文件:
#include <stdio.h>
// 断言所需
#include <assert.h>
// 申请空间所需
#include <stdlib.h>
// 布尔类型所需
#include <stdbool.h>
- 得到一个树的结点,我们首先要定义一个结点的结构体。一个结点包含存储的数据和分别指向左孩子结点和右孩子结点的两个指针,定义如下:
typedef int BTDataType;
typedef struct TreeNode
{
// 存储数据
BTDataType data;
// 指向左结点的指针
struct TreeNode* left;
// 指向右结点的指针
struct TreeNode* right;
}BTNode;
- 有了结点的定义之后,如何得到一个结点呢?当然是向内存申请一个结点的空间,申请之后,我们顺便将这个结点里的成员初始化:
data
给自己想要的值,两个指针分别置为NULL
。最后返回指向该结点空间的指针即可。
相关函数接口代码实现:
// 得到一个结点
BTNode* BuyTreeNode(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
// 判断一下是否申请空间失败
assert(newnode);
newnode->data = x;
// 两个指针都初始化 NULL
newnode->left = NULL;
newnode->right = NULL;
// 返回指向这个结点空间的指针
return newnode;
}
自己捣鼓的二叉树
- 有了得到一个树结点的功能后,接下来就是按照自己的意思创建一棵树了。
这里我们采用下面这棵树作为例子:
相关函数接口代码实现:
BTNode* BuyTree()
{
// 依次得到一个结点
BTNode* n1 = BuyTreeNode(1);
BTNode* n2 = BuyTreeNode(2);
BTNode* n3 = BuyTreeNode(3);
BTNode* n4 = BuyTreeNode(4);
BTNode* n5 = BuyTreeNode(5);
BTNode* n6 = BuyTreeNode(6);
BTNode* n7 = BuyTreeNode(7);
// 通过每个结点内的指针进行树的连接
n1->left = n2;
n2->left = n3;
n1->right = n4;
n4->left = n5;
n4->right = n6;
n2->right = n7;
// 返回所有结点的祖先结点
return n1;
}
不同的二叉树这里的代码不同,根据自己的需求来建树。
二叉树的前序遍历
- 前序遍历的顺序是 根 - 左 - 右 ,先打印 根 ,然后递归左和右, 如果当前结点为
NULL
,打印NULL
,然后递归返回。
图示理解:
相关函数接口代码实现:
// 二叉树的前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
// 根 左 右
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
二叉树的中序遍历
- 中序遍历的顺序是 左 - 根 - 右 ,先遍历该二叉树的左子树,然后根,最后右子树, 然后该二叉树的左子树和右子树又是先遍历其左子树,然后根,最后右子树,这样就分成了若干个子问题。如果当前结点为
NULL
,打印NULL
,然后递归返回。
图示理解:
相关函数接口代码实现:
// 二叉树的中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
// 左 根 右
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
二叉树的后序遍历
- 前序遍历的顺序是 左 - 右 - 根 ,先遍历该而二叉树的左子树,然后右子树,最后根,然后该二叉树的左子树和右子树又是先遍历其左子树,然后右子树,最后根。这样就分成了若干个子问题。如果当前结点为
NULL
,打印NULL
,然后递归返回。
图示理解:
相关函数接口代码实现:
// 二叉树的后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
// 左 右 根
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
二叉树的结点个数
-
求二叉树的结点个数,我们可以采用一种分治思想,
求
该树的左子树的结点个数加上该树的右子树的结点个数再加上自己。然后该树的左子树和右子树又可以分成求
该子树的左子树的结点个数加上该子树的右子树的结点个数再加上自己,这样不断将问题细化,符合递归的性质。 -
如果该根节点为
NULL
,说明不是一个结点,返回0
。
图示理解:
相关函数接口代码实现:
// 二叉树的结点个数
int TreeNodeSize(BTNode* root)
{
// +1 是算上自己为一个结点
return root == NULL ? 0 : TreeNodeSize(root->left) + TreeNodeSize(root->right) + 1;
}
二叉树的叶子个数
-
求二叉树的叶子个数也是采用分治思想,
求
该树的左子树的叶子个数加上该树的右子树的叶子个数,然后该树的左子树和右子树又可以分为求
该树的左子树的叶子个数加上该树的右子树的叶子个数,依此继续细分,最终计算完后返回的就是每棵子树的叶子结点个数。 -
判断是否是叶子结点只需判断该结点的左孩子和右孩子是不是都为
NULL
即可,如果是就返回1
。 -
当然,如果该结点为
NULL
,就直接返回0
。
图示理解:
颜色出现的先后顺序可以看作递归的过程,上树的叶子结点为3
。
相关函数接口代码实现:
// 二叉树的叶子个数
int TreeLeafSize(BTNode* root)
{
// 如果该结点为 NULL , 说明不是叶子结点,直接返回 0
if (root == NULL) return 0;
// 如果该结点的左孩子和右孩子都为 NULL ,说明该结点为叶子结点,返回 1
if (root->left == NULL && root->right == NULL) return 1;
// 递归左子树和右子树,统计左右子树的叶子结点
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
求出二叉树的高度
- 求二叉树的高度也是采用分治思想:先求出该树的左子树的高度,再求出该树的右子树高度,然后比较得到高度高的那个加上自己(就是加一)。该树的左子树和右子树又是这样的一个操作:先求出该树的左子树的高度,再求出该树的右子树高度,然后比较得到高度高的那个加上自己(就是加一)。依次细分,最终递归返回得到的就是整棵树的高度(深度)。
图示理解:
相关函数接口代码实现:
// 二叉树的高度(深度)
int TreeHeight(BTNode* root)
{
// 分治思想
// 如果该节点为空,没有高度,返回0
if (root == NULL) return 0;
// 递归统计左右子树的高度
int leftheight = TreeHeight(root->left);
int rightheight = TreeHeight(root->right);
// 返回左右子树高度大小大的那个并加一,加一是加上该结点的高度1
return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}
第K层的结点个数
-
求第
K
层的结点个数,如果K
是1
,就是根节点,答案为1
;如果K
是2
,就是第二层的结点个数,此时我们在递归的时候,每向下递归一层,就把K
视为K - 1
一次,所以到达第二层的时候,K
为1
。由上不难发现,当K
为1
所在的层就是需要统计结点个数的层。因此有了判断:当该结点所在的层数有K == 1
,返回1
。如果在第K
层前或在第K
层遇到NULL
就返回0
。 -
同样的,这里采用递归,分别递归左和右去寻找处在第
K
层的结点。
图示理解:
相关函数接口代码实现:
// 二叉树第K层的结点个数
int TKLevelNodeSize(BTNode* root, int k)
{
// 如果到达第K层之前或者到达第K层该结点为NULL,直接返回0
//(说明不是第K层的结点或者在第K层为NULL)
if (root == NULL) return 0;
// 如果k减到1了,说明到达第K层了,此时该结点有效,应计数1
if (k == 1) return 1;
return TKLevelNodeSize(root->left, k - 1) + TKLevelNodeSize(root->right, k - 1);
}
查找值为X的结点
-
查找值为
X
的结点,先在该树的左子树找,如果找到了就直接返回,如果找不到,再到该树的右子树去找,如果右子树找到了也直接返回,如果找不到,左右子树都没有,就返回NULL
。 -
同样的,将上一条继续细分,这样递归下去就只有找到和找不到两种情况,如果找到了就返回找到的那个结点的指针,如果找不到就返回
NULL
。
图示理解:
相关函数接口代码实现:
// 二叉树结点数据的查找
BTNode* TreeDataFind(BTNode* root, BTDataType x)
{
// 如果该结点为NULL, 返回NULL
if (root == NULL) return NULL;
// 找到了就返回指向该结点的指针
if (root->data == x) return root;
// 先在左边找
BTNode* lret = TreeDataFind(root->left, x);
// 如果寻找不到返回NULL,这里if就不进去
if (lret) return lret;
// 如果左没找到就找右
BTNode* rret = TreeDataFind(root->right, x);
// 如果寻找不到返会NULL,这里if就不进去
if (rret) return rret;
// 如果左右都没找到就返回NULL
return NULL;
}
二叉树的层序遍历
-
层序遍历就是一层一层的从左到右打印结点的数据,这里递归就办不到了,那么如何来实现呢?
-
首先我们需要一个队列,这个队列存储的数据是树结点的指针,我们先将根结点入队列(如果根结点为
NULL
就不入),然后取队头数据存下来(front) 并 出队列操作,接着打印front
指向的结点的数据(front->data
),最后将front
指向的结点的左孩子和右孩子分别入队列(如果为NULL
就不入)。整个操作是一个循环,当队列为空时结束遍历。 -
队列的代码可以直接从之前写的队列那一章拷贝过来(这里就不放了):-> 队列传送门 <-
图示理解:
相关函数接口代码实现:
// 层序遍历,利用队列来操作
void LevelOrder(BTNode* root)
{
// 队列的每一个数据是一个树的结点
Que q;
QInit(&q);
// 如果根节点不为NULL就入队列
if (root) QPush(&q, root);
// 队列不为空就继续
while (!QEmpty(&q))
{
// 取队头元素(树的结点)
BTNode* front = QFront(&q);
// 出队列是释放队头的空间,在队头存放的树的结点没有被释放
QPop(&q);
// 打印树结点中的数据
printf("%d ", front->data);
// 如果该队头树结点的左孩子不为空就入队列
if (front->left) QPush(&q, front->left);
// 如果该对头树节点的右孩子不为空就入队列
if (front->right) QPush(&q, front->right);
}
printf("\n");
// 记得销毁队列噢,防止内存泄漏
QDestroy(&q);
}
是否为完全二叉树
-
判断一棵二叉树是否为完全二叉树,首先要知道什么是完全二叉树,前面 树的介绍 这一章详细讲解了完全二叉树,这里就不做过多探讨。
-
我们需要知道的是,一棵完全二叉树,我们以层序的视角去看待它,会发现它是连续的:
-
所以根据此性质,这里可以运用层序遍历的思想,用一个队列来解决。
-
但与层序遍历不同的是,这里的入队列,是
NULL
也入,每次出队列就同时入队列这个出队列的数据的左孩子和右孩子,当出队列的这个树结点的指针为NULL
的时候,说明前面的连续的有效结点组成的树是一颗完全二叉树,此时跳出循环。 -
跳出循环后,此时队列里面还有数据,如果队列里的数据全是
NULL
就说明这是一棵完全二叉树,如果队列里的数据有一个不是NULL
,它就不是一棵完全二叉树。
图示理解:
相关函数接口代码实现:
// 判断该二叉树是否为完全二叉树
bool BTComplate(BTNode* root)
{
Que q;
QInit(&q);
// 如果root不为NULL就入队列
if (root) QPush(&q, root);
while (!QEmpty(&q))
{
// 取队头元素,就是一个树的结点
BTNode* front = QFront(&q);
QPop(&q);
// 如果当前结点是NULL,说明前面每层连续的有效结点都出队列了,直接跳出循环
if (front == NULL) break;
// 入front结点的左孩子和右孩子,NULL也入
QPush(&q, front->left);
QPush(&q, front->right);
}
// 如果队列不为NULL,接下来就是判断的重要一步了
// 根据完全二叉树的性质,当前面每层连续的有效结点都走完时
// 剩下队列里的元素如果都是NULL就说明该二叉树是一棵完全二叉树
// 如果有一个结点不为NULL,那么就说明该二叉树不是完全二叉树
while (!QEmpty(&q))
{
BTNode* front = QFront(&q);
QPop(&q);
if (front)
{
// 如果找到一个有效结点,就说明该二叉树不是完全二叉树,直接返回false
// 返回前记得销毁队列噢
QDestroy(&q);
return false;
}
}
// 如果前面循环走完,说明符合完全二叉树的性质,最后返回true
// 返回前记得销毁队列噢
QDestroy(&q);
return true;
}
关于二叉树的销毁
- 二叉树的销毁也是一个递归的过层,这里采用的是后序遍历来依次销毁,因为如果采用前序或中序的话,递归就回不去了。
图示理解:
相关函数接口代码实现:
// 二叉树的销毁
// 注意一定是要通过后续遍历来销毁
// 因为后序遍历是 左 右 根 ,销毁左右再销毁根
// 这样能够递归返回回去
void BTDestroy(BTNode* root)
{
if (root == NULL) return;
BTDestroy(root->left);
BTDestroy(root->right);
// 释放(返还)root指向的结点的空间
free(root);
}
完整的二叉树代码
▶️队列的代码可以直接从之前写的队列那一章拷贝过来(这里就不放了):-> 队列传送门 <-。注意:拷贝过来后,要改一下队列存放的数据的类型(改为一个指向树的结点的指针)。
初等二叉树的完整代码BinaryTree.c
// 引入队列头文件之后,因为队列头文件里有下面的库函数包含,所以这里就不必重复包含了
//#include <stdio.h>
//#include <assert.h>
//#include <stdlib.h>
//#include <stdbool.h>
#include "queue.h"
typedef int BTDataType;
typedef struct TreeNode
{
// 存储数据
BTDataType data;
// 指向左结点的指针
struct TreeNode* left;
// 指向右结点的指针
struct TreeNode* right;
}BTNode;
// 得到一个结点
BTNode* BuyTreeNode(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
// 判断一下是否申请空间失败
assert(newnode);
newnode->data = x;
// 两个指针都初始化 NULL
newnode->left = NULL;
newnode->right = NULL;
// 返回指向这个结点空间的指针
return newnode;
}
// 自行建一棵树,可随意建树,但必须要是二叉树
BTNode* BuyTree()
{
// 依次得到一个结点
BTNode* n1 = BuyTreeNode(1);
BTNode* n2 = BuyTreeNode(2);
BTNode* n3 = BuyTreeNode(3);
BTNode* n4 = BuyTreeNode(4);
BTNode* n5 = BuyTreeNode(5);
BTNode* n6 = BuyTreeNode(6);
BTNode* n7 = BuyTreeNode(7);
// 通过每个结点内的指针进行树的连接
n1->left = n2;
n2->left = n3;
n1->right = n4;
n4->left = n5;
n4->right = n6;
n2->right = n7;
// 返回所有结点的祖先结点
return n1;
}
// 二叉树的前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
// 根 左 右
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
// 二叉树的中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
// 左 根 右
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
// 二叉树的后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
// 左 右 根
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
// 二叉树的结点个数
int TreeNodeSize(BTNode* root)
{
// +1 是算上自己为一个结点
return root == NULL ? 0 : TreeNodeSize(root->left) + TreeNodeSize(root->right) + 1;
}
// 二叉树的叶子个数
int TreeLeafSize(BTNode* root)
{
// 如果该结点为 NULL , 说明不是叶子结点,直接返回 0
if (root == NULL) return 0;
// 如果该结点的左孩子和右孩子都为 NULL ,说明该结点为叶子结点,返回 1
if (root->left == NULL && root->right == NULL) return 1;
// 递归左子树和右子树,统计左右子树的叶子结点
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
// 二叉树的高度(深度)
int TreeHeight(BTNode* root)
{
// 分治思想
// 如果该节点为空,没有高度,返回0
if (root == NULL) return 0;
// 递归统计左右子树的高度
int leftheight = TreeHeight(root->left);
int rightheight = TreeHeight(root->right);
// 返回左右子树高度大小大的那个并加一,加一是加上该结点的高度1
return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}
// 二叉树第K层的结点个数
int TKLevelNodeSize(BTNode* root, int k)
{
// 如果到达第K层之前或者到达第K层该结点为NULL,直接返回0
//(说明不是第K层的结点或者在第K层为NULL)
if (root == NULL) return 0;
// 如果k减到1了,说明到达第K层了,此时该结点有效,应计数1
if (k == 1) return 1;
return TKLevelNodeSize(root->left, k - 1) + TKLevelNodeSize(root->right, k - 1);
}
// 二叉树结点数据的查找
BTNode* TreeDataFind(BTNode* root, BTDataType x)
{
if (root == NULL) return NULL;
if (root->data == x) return root;
BTNode* lret = TreeDataFind(root->left, x);
if (lret) return lret;
BTNode* rret = TreeDataFind(root->right, x);
if (rret) return rret;
return NULL;
}
// 层序遍历,利用队列来操作
void LevelOrder(BTNode* root)
{
// 队列的每一个数据是一个树的结点
Que q;
QInit(&q);
// 如果根节点不为NULL就入队列
if (root) QPush(&q, root);
// 队列不为空就继续
while (!QEmpty(&q))
{
// 取队头元素(树的结点)
BTNode* front = QFront(&q);
// 出队列是释放队头的空间,在队头存放的树的结点没有被释放
QPop(&q);
// 打印树结点中的数据
printf("%d ", front->data);
// 如果该队头树结点的左孩子不为空就入队列
if (front->left) QPush(&q, front->left);
// 如果该对头树节点的右孩子不为空就入队列
if (front->right) QPush(&q, front->right);
}
printf("\n");
// 记得销毁队列噢,防止内存泄漏
QDestroy(&q);
}
// 判断该二叉树是否为完全二叉树
bool BTComplate(BTNode* root)
{
Que q;
QInit(&q);
// 如果root不为NULL就入队列
if (root) QPush(&q, root);
while (!QEmpty(&q))
{
// 取队头元素,就是一个树的结点
BTNode* front = QFront(&q);
QPop(&q);
// 如果当前结点是NULL,说明前面每层连续的有效结点都出队列了,直接跳出循环
if (front == NULL) break;
// 入front结点的左孩子和右孩子,NULL也入
QPush(&q, front->left);
QPush(&q, front->right);
}
// 如果队列不为NULL,接下来就是判断的重要一步了
// 根据完全二叉树的性质,当前面每层连续的有效结点都走完时
// 剩下队列里的元素如果都是NULL就说明该二叉树是一棵完全二叉树
// 如果有一个结点不为NULL,那么就说明该二叉树不是完全二叉树
while (!QEmpty(&q))
{
BTNode* front = QFront(&q);
QPop(&q);
if (front)
{
// 如果找到一个有效结点,就说明该二叉树不是完全二叉树,直接返回false
// 返回前记得销毁队列噢
QDestroy(&q);
return false;
}
}
// 如果前面循环走完,说明符合完全二叉树的性质,最后返回true
// 返回前记得销毁队列噢
QDestroy(&q);
return true;
}
// 二叉树的销毁
// 注意一定是要通过后续遍历来销毁
// 因为后序遍历是 左 右 根 ,销毁左右再销毁根
// 这样能够递归返回回去
void BTDestroy(BTNode* root)
{
if (root == NULL) return;
BTDestroy(root->left);
BTDestroy(root->right);
// 释放(返还)root指向的结点的空间
free(root);
}
int main()
{
BTNode* root = BuyTree();
PreOrder(root);
printf("\n");
InOrder(root);
printf("\n");
PostOrder(root);
printf("\n");
printf("TreeNodeSize = %d\n", TreeNodeSize(root));
printf("TreeLeafSize = %d\n", TreeLeafSize(root));
printf("TreeHeight = %d\n", TreeHeight(root));
printf("TreeKNodeSize = %d\n", TKLevelNodeSize(root, 3));
printf("TreeFindData = %d\n", TreeDataFind(root, 6)->data);
LevelOrder(root);
if (BTComplate(root)) printf("Full binary tree!\n");
else printf("Not full binary tree!\n");
BTDestroy(root);
return 0;
}
☑️写在最后
💝回顾上文,其中的分治思想值得我们探讨。普通二叉树学完之后,初阶的数据结构就告一段落了,整体来说,初阶数据结构还是不难的,掌握其之后,我相信应付学校的考试那是绰绰有余。后面我会给大家带来高阶数据结构的文章,大家准备好接招吧嘿嘿!
❤️🔥后续将会继续输出有关数据结构与算法的文章,你们的支持就是我写作的最大动力!
感谢阅读本小白的博客,错误的地方请严厉指出噢~