在此之前我们已经初步了解了二叉树,在介绍堆的基本应用时,我们已经具体介绍了完全二叉树的基本应用,本章我们介绍二叉树的基本应用,这个不止指的是完全二叉树,而是指泛型的二叉树。
二叉树的基本应用,由于其左右子树层层连接的特性,大多都是由双向递归实现的,在本章我们要大量使用递归思想,希望能帮助到你
目录
二叉树的遍历
1.前序遍历(深度优先遍历DFS)
2.中序遍历
3.后序遍历
二叉树遍历的代码实现
1.实现前序遍历
2. 实现中序遍历
3.实现后序遍历
二叉树的基本函数接口
二叉树函数实现
1.二叉树结构体
2.通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
3. 二叉树销毁
4.二叉树节点个数
5.二叉树叶子节点个数
6. 二叉树第k层节点个数
7.二叉树查找值为x的节点
层序遍历(广度优先遍历BFS)
判断完全二叉树
总结:
二叉树的遍历
我们在介绍二叉树基本函数之前,先来介绍一下二叉树的遍历
二叉树的基本遍历分为三种,前序遍历,中序遍历,后序遍历
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
我们下面用这棵树进行讲解
1.前序遍历(深度优先遍历DFS)
即根据 根节点->左子树->右子树的顺序进行遍历,遇到叶结点即返回
逻辑思路:按照根节点 ->左子树->右子树的顺序我们可以排序,第一个就是根节点 1 ,之后去找左子树,再按照顺序可得 节点 2 ,再找2的左子树为节点 3 ,再去找3的子树,此时发现3的左右子树都为NULL,则返回到2节点,此时2节点的左子树已经找完,按照顺序,我们该去找右子树,而2的右子树为NULL,此时2节点已经找完,返回到1,1的左子树找完,去到1的右子树4节点,取根节点4,再找左子树5以及右子树6;
完成这个顺序就完成了前序遍历
前序遍历结果:1 2 3 4 5 6
2.中序遍历
即根据 左子树->根节点->右子树的顺序进行遍历,遇到叶结点返回。
具体逻辑和前序遍历类似,只是访问顺序不同
中序遍历结果:3 2 1 5 4 6
3.后序遍历
即根据 左子树->右子树->根节点的顺序进行遍历,遇到叶结点返回
具体逻辑和前序遍历类似,只是访问顺序不同
后序遍历结果:3 2 5 6 4 1
下面是3种遍历的顺序示意图:
二叉树遍历的代码实现
1.实现前序遍历
我们详细介绍前序遍历
后序遍历和中序遍历我们用流程图解释
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int BTDataType;
typedef struct BinaryTreeNode //定义二叉树结构体
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
BTNode* BuyBTNode(BTDataType x) //创建二叉树节点函数
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
printf("malloc fail\n");
exit(-1);
}
node->data = x;
node->left = node->right = NULL;
return node;
}
BTNode* CreatBinaryTree() //手动创建一个如上图所示的二叉树
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
void PrevOrder(BTNode* root) { //先序遍历函数
if (root == NULL) {
printf("NULL ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
以下具体介绍二叉树前序遍历的函数调用,前序遍历将会调用13个栈帧
栈帧1:将根节点传入到prevOrder函数中,打印数据1,然后分别调用1左子树的prevOrder,再调用1右子树的prevOrder
栈帧2:打印2后,分别调用2左子树的prevOrder,再调用2右子树prevOrder
栈帧3:打印3后,分别调用3左子树的prevOrder,再调用3右子树prevOrder
栈帧4:由于3的左子树为NULL,那么就打印NULL,递归回上一层函数,到3右子树prevOrder,同时销毁栈帧4
栈帧5:由于3的右子树为NULL,那么就打印NULL,递归回上一层函数,到2右子树prevOrder,同时栈帧销毁5,3
栈帧6:栈帧6调用的是2的右子树,2的右子树为NULL,就打印NULL,递归回上一层函数,此时最初的根节点1的左子树全部访问完成,进而将去访问1的右子树,这里栈帧2结束,被销毁
栈帧7:打印4后,分别调用4左子树的prevOrder,再调用4右子树prevOrder
栈帧8:打印5后,分别调用5左子树的prevOrder,再调用5右子树prevOrder
栈帧9:由于5的左子树为NULL,那么就打印NULL,递归回上一层函数,到5右子树prevOrder,同时栈帧销毁9
栈帧10:栈帧10调用的是5的右子树,5的右子树为NULL,就打印NULL,递归回上一层函数,这里5节点已经完全访问完成,销毁栈帧10,8
栈帧11:此时访问完成4的左子树,去访问4的右子树,即打印6,然后分别调用6左子树的prevOrder,再调用6右子树prevOrder
栈帧12:栈帧12调用的是6的左子树,6的右子树为NULL,就打印NULL,递归回上一层函数,销毁栈帧12
栈帧13:此时访问完成6的左子树,去访问6的右子树,6的右子树为NULL,即打印NULL,至此已经将二叉树全部访问完成,依次返回结果,然后逐步将栈帧销毁。
下面是函数递归展开图能够更好的理解
2. 实现中序遍历
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
BinaryTreeInOrder(root->_left);
printf("%d ", root->_data);
BinaryTreeInOrder(root->_right);
}
递归展开图如下:
3.实现后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
BinaryTreePostOrder(root->_left);
BinaryTreePostOrder(root->_right);
printf("%d ", root->_data);
}
递归展开图如下:
二叉树的基本函数接口
下面我们实现二叉树的基本函数
先来看一下二叉树的函数接口
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
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);
二叉树函数实现
1.二叉树结构体
我们在之前已经介绍了二叉树节点的基本结构,包含三个元素:数据,左子树,右子树
这样我们可以清楚的访问到树的全部节点
结构体构建
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
2.通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
这个函数的意思就是根据前序遍历的数组,来创建一个二叉树,我们既然知道了二叉树的前序遍历结果,同时空节点由#代替,我们就能利用递归来构建这个符合要求的二叉树、
进行遍历数组,由于这里是递归函数,那么我们就需要记住数组下标,而如果仅仅是用int类型的话,那么形参是会在递归过程中不断的销毁重建,是没法达到记住数组下标的目的的。
因此,我们传入int*,将地址传入,这样无论递归多少层,修改的变量一直是同一个变量,就能实现记录下标的目的
若数组元素为#则,返回NULL,同时下标+1
下面按照前序遍历进行递归构建二叉树
代码如下:
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->_data = a[(*pi)++];
root->_left = BinaryTreeCreate(a, pi);
root->_right = BinaryTreeCreate(a, pi);
return root;
}
3. 二叉树销毁
二叉树销毁的过程,也是一个递归的过程,但是我们不能用前序遍历进行递归,如果我们用前序遍历进行递归的话,先将root释放,那我们就没法找到根的左右子树了,因此我们需要用后序遍历进行销毁。
我们用递归,找到根的左子树和右子树,从叶节点开始,从叶到根依次释放掉根节点的空间即可
不要忘记递归的终止条件,当root == NULL 时 返回
这里我们需要传入结构体二级指针,因为我们的根节点是一级指针,因此想要释放,就需要传入其地址,由二级指针接收
代码如下:
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
return;
BinaryTreeDestory((*root)->_left);
BinaryTreeDestory((*root)->_right);
free(*root);
}
4.二叉树节点个数
树的节点个数,也不难理解,还是利用递归,我们定义一个全局变量size,在左右子树进行递归,每次调用一次函数进行判断,若根节点不为NULL,就将size++,层层递归,最后size就是节点个数的结果
代码如下:
// 二叉树节点个数
int size = 0;
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
else
{
size++;
}
BinaryTreeSize(root->_left);
BinaryTreeSize(root->_right);
return size;
}
5.二叉树叶子节点个数
叶节点和节点个数略有差异,总体思想都是递归,但是叶结点需要进行判断,很显然叶结点的左右子树都为空,因此我们只要进行判断一下,符合条件就返回1,利用递归将左右子树的叶节点加起来就可以了
代码如下:
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root->_left == NULL && root->_right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}
6. 二叉树第k层节点个数
统计k层的节点个数,还是利用递归,我们创建一个全局变量sizek,假设求第k层的节点,那么从第一层开始进行递归,每一次传入k = k -1,
当k==1 时sizek++
若k >1 则说明在k层之上,我们就需要继续递归,将 k -1,找到第k层
若k<1 说明已经找到了k层的元素,超过了第k层,直接返回0就可以了
注意,记住判断根节点是否为空,若为空就返回 0 ,不然会出现越界现象
逻辑图如下:
代码实现如下:
// 二叉树第k层节点个数
int sizek = 0;
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k < 1)
return 0;
else if (k > 1)
{
BinaryTreeLevelKSize(root->_left, k - 1);
BinaryTreeLevelKSize(root->_right, k - 1);
}
else if (k == 1)
{
sizek++;
}
return sizek;
}
7.二叉树查找值为x的节点
查找对应值的节点,还是利用递归思想,由根->左子树->右子树的顺序即前序遍历的顺序进行查找,找到对应值就返回对应节点,若没有找到就返回NULL。
注意:在递归查找对应节点时,我们需要用指针进行接收结果,然后先判断左子树结果是否为NULL,若不为NULL,就直接返回对应节点,这样就不用再去遍历右子树,节省了时间和空间
代码如下:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->_data == 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;
}
层序遍历(广度优先遍历BFS)
层序遍历比较特殊,我们单独分一部分来讲
这里我们需要用到队列的基本用法,如果忘记了可以去复习一下
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
我们利用队列,上一层带下一层,将数据一层一层的入队列,根据队列的先进先出的特性,我们能将数据按照层序打印出来
逻辑图如下
我们建立队列q,此时队列q存的数据类型是结构体指针,这样才能记录根节点的地址,从而进行修改,然后判断根节点是否为空,若不为空就让根节点入队列,然后建立循环,当队列为空时说明数据已经遍历完成,此时终止循环
在循环中,我们记录队首数据,然后打印数据,删除队首数据,再用递归访问根节点的左右子树,从而实现层序遍历
代码如下:
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->_data);
if (front->_left)
QueuePush(&q, front->_left);
if (front->_right)
QueuePush(&q, front->_right);
}
}
判断完全二叉树
判断完全二叉树,我们先来分析一下
完全二叉树:前n-1层 都是满的 第n层从左到右是连续的
我们仍然利用层序遍历的思想,不同的是,刚才我们的层序遍历不进空节点,这里我们也进入空节点
1.进行层序遍历,空也进队列
2.遇到第一个空节点时,开始判断,后面全空就是完全二叉树,后面有非空就不是完全二叉树
我们用逻辑图分析一下,即
下面我们根据思路来实现代码,总体思路还是层序遍历,但是不用打印数据,直接将左右子树数据入队,然后若出队数据为NULL节点,那么就进行判断,判断队列内是否全为NULL节点,若全为NULL节点,那么就是完全二叉树,若有非空数据,就不是完全二叉树。
代码实现如下:
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
break;
}
QueuePush(&q, front->_left);
QueuePush(&q, front->_right);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
总结
二叉树的基本应用,还是比较复杂的,大多要使用递归实现,个别还需要用到队列的知识,因此我们需要掌握递归的原理,若不理解递归过程,可以通过画递归展开图的方式去理解,一定要将每一个函数接口给理解清楚,此类问题需要我们不断的练习和复习,才能将二叉树的知识巩固住,希望本篇文章能够帮助到你。