数据结构——二叉树的基本应用

在此之前我们已经初步了解了二叉树,在介绍堆的基本应用时,我们已经具体介绍了完全二叉树的基本应用,本章我们介绍二叉树的基本应用,这个不止指的是完全二叉树,而是指泛型的二叉树。

二叉树的基本应用,由于其左右子树层层连接的特性,大多都是由双向递归实现的,在本章我们要大量使用递归思想,希望能帮助到你

目录

二叉树的遍历

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;
}

总结

二叉树的基本应用,还是比较复杂的,大多要使用递归实现,个别还需要用到队列的知识,因此我们需要掌握递归的原理,若不理解递归过程,可以通过画递归展开图的方式去理解,一定要将每一个函数接口给理解清楚,此类问题需要我们不断的练习和复习,才能将二叉树的知识巩固住,希望本篇文章能够帮助到你。

  • 35
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值