二叉树(三种遍历方法等)

二叉树

二叉树链式结构的实现

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

//二叉树结构
typedef int BTDataType;
typedef struct BinaryTreeNode {
	BTDataType data;
	struct BinaryTreeNode* left_child;
	struct BinaryTreeNode* right_child;
}TreeNode;

//创建节点
TreeNode* BuyNode(BTDataType x) {
	TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
	assert(node);
	node->data = x;
	node->left_child = NULL;
	node->right_child = NULL;

	return node;
}

//创建二叉树
TreeNode* CreateTree() {
	TreeNode* node1 = BuyNode(1);
	TreeNode* node2 = BuyNode(2);
	TreeNode* node3 = BuyNode(3);
	TreeNode* node4 = BuyNode(4);
	TreeNode* node5 = BuyNode(5);
	TreeNode* node6 = BuyNode(6);

	node1->left_child = node2;
	node1->right_child = node4;
	node2->left_child = node3;
	node4->left_child = node5;
	node4->right_child = node6;
	return node1;
}

创建出来的二叉树如图所示,顺便让大家再记起一下之前提起过的二叉树的结构:根,左子树,右子树
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
二叉树(三种遍历方法等)_第1张图片

先序 中序 后序遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。(根左右)
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。(左根右)
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。(左右根)

接下来我们以这副图为例子 先后进行遍历的结果分析

二叉树(三种遍历方法等)_第2张图片
先序:根左右,所以从根开始,1
再向左子树出发,2,3,N,N,N
右子树,4,5,N,N,6,N,N
最后结果为:1 2 3 N N N 4 5 N N 6 N N

同理,中序:N 3 N 2 N 1 N 5 N 4 N 6 N
后序:N N 3 N 2 N N 5 N N 6 4 1

如果按照结构来划分,还可以写成这种样子:
二叉树(三种遍历方法等)_第3张图片
而写成代码该如何写呢?那么首先我先把代码展示给大家

void PreOrder(TreeNode* root) {
	if (root == NULL){
		printf("N ");
		return;
	}

	printf("%d ", root->data);
	PreOrder(root->left_child);
	PreOrder(root->right_child);
}

void InOrder(TreeNode* root) {
	if (root == NULL) {
		printf("N ");
		return;
	}

	InOrder(root->left_child);
	printf("%d ", root->data);
	InOrder(root->right_child);
}

void PostOrder(TreeNode* root) {
	if (root == NULL) {
		printf("N ");
		return;
	}

	PostOrder(root->left_child);
	PostOrder(root->right_child);
	printf("%d ", root->data);
}

二叉树(三种遍历方法等)_第4张图片
接着我们以先序遍历为例子 给大家讲解一下这个过程
首先root为1,那么输出1,然后左节点递归到2;输出2,然后递归左节点到3;输出3,然后递归左节点为N输出N返回结束递归,再递归右节点为N输出N返回结束递归

于是开始返回到2,2的右节点递归为N返回;返回到1,1开始递归右节点4,输出4,然后递归4的左节点5,左节点5递归左节点右节点都为空,均输出N并返回到4,而递归右节点6也是一样,最后又把结果返回到1并结束进程

可以看出,在弄懂遍历过程之后,逐步递归的过程还是很简单的

二叉树的其他简单应用

求二叉树节点个数

先给大家看一个反面例子:

int TreeSize(TreeNode* root){
	int size=0;
	if(root==NULL){
		return 0;
	++size;
	
	TreeSize(root->left_child);
	TreeSize(root->right_child);
	
	return size;

大家可能一般都会想到用这个size来记录节点的个数,可是如果这么使用,那么每个节点的递归中都会有一个属于自己的size,那么size的值是不固定的,我们不知道到底最后取到的是哪个size,如果以上面的图为例,那么会返回1,很明显,这是错的

有人会想说,可以使用static让size成为一个静态变量,那样的话就不会因为递归而造成size的值分为好几个,而是只在一开始初始化为0一次。但是这也是有问题的,如果我们重复使用,size的值就会叠加,从第二次开始就会不一样。但是这是可以解决的,我们可以让size不再是一个局部变量,而成为一个静态全局变量,在一开始时就定义,这样我们就可以在main函数中每次使用完之后将其重置为0,但这样也很麻烦,所以我们最后选择更精妙的方法!请看!

int TreeSize(TreeNode* root) {
	return root == NULL ? 0 : 
	TreeSize(root->left_child) + 
	TreeSize(root->right_child) + 1;
}

这是一个使用三目运算符所构成的等式,其意思就是如果这个节点为空,那么返回0,不为空的话就返回这个节点的左子树和右子树的节点之和加1(这个节点本身)

还是以上面的图为例子举例,1不为空,+1,左子树2也不为空,+1,左子树3也不为空,+1,但3的左与右都为空,故3返回2的值为1,而2返回1的值为1+1=2,轮到右子树4不为空,+1,左子树5不为空,+1,但左子树的左与右为空,故返回4的值为1,而6同理也返回1,然后4返回1的值为1+1+1=3,最后返回到1节点的值就是2+3+1=6

求叶子节点个数

根据上面的求二叉树节点个数,大家肯定会想照葫芦画瓢,模仿其写一个,比如下面这样子:

int TreeLeafSize(TreeNode* root){
	return !root->left_child && !root->right_child ? 1:
	TreeLeafSize(root->left_child)+
	TreeLeafSize(root->right_child);
}

这个逻辑确实很正确,左右都无子树时,此节点才为叶子节点,不然就是其左子树的叶子节点加上右子树的叶子节点数

但是,它忽略了一个问题,如果这个树是空树,又或者说,这个节点是空节点,它并没有给出判断的条件,这会导致程序崩溃的

当上面的二叉树遍历到2的时候,就会需要确认左子树和右子树的叶子节点,但是我们发现,2的右子树为空,那么这样就无法进行判断,它将尝试访问 root->left_child 或 root->right_child,这会导致未定义的行为,通常是程序崩溃。

正确代码如下:

int TreeLeafSize(TreeNode* root) {
	//空节点
	if (root == NULL)
		return 0;

	//叶子节点
	if (!root->left_child && !root->right_child)
		return 1;

	//既不是空节点,也不是叶子节点
	return TreeLeafSize(root->left_child)
		+ TreeLeafSize(root->right_child);
}

有时候不用为了贪小便宜而失去代码的真正效用,多写一点也无所谓

求二叉树的高度

首先我们分治一下问题:

  1. 空 返回0
  2. 不是空 左子树高度和右子树高度 大的那个+1

那么这个时候又会有同学想到三目操作符,写出一段很简洁,运行起来却很麻烦的代码

int TreeHeight(TreeNode* root){
	return root==NULL ? 0 :
		TreeHeight(root->left_child) > TreeHeight(root->right_child)?
		TreeHeight(root->left_child)+1:TreeHeight(root->right_child)+1;

看上去的确清晰明了,但里面有一个严重的问题影响了效率
那就是它会先比较谁大谁小,然后再返回这个高度值,但是问题在于,它一开始先比大小并没有记住数据,而只是比了比大小,这就导致在知道谁大谁小之后,我们依然需要再次向下递归,寻找深度,这是很浪费时间的

而正确方法如下,有两种方法可供选择:

//需要包含头文件
int TreeHeight(TreeNode* root)
{
	if (root == NULL)
		return 0;

	return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1;
}

int TreeHeight(TreeNode* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

第k层的节点个数

首先分治子问题

  1. 空,返回0
  2. 不为空且k==1,返回1
  3. 不为空且大于1,返回左子树的k-1层与右子树的k-1层

代码如下:

int TreeLevelK(TreeNode* root,int k) {
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	
	return TreeLevelK(root->left_child, k - 1) +
		TreeLevelK(root->right_child, k - 1);
}

二叉树查找值为x的节点

首先,还是给大家先看一个大家很容易写出来的错误例子

TreeNode* TreeFind(TreeNode* root, BTDataType x) {
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	TreeFind(root->left_child, x);
	TreeFind(root->right_child, x);
}

乍一看好像没问题,节点为空返回空,找到了就返回这个节点,这个不是就从左子树和右子树里面找,但实际上隐藏着一个问题,我们还是以上面的图为例,假设我们要找的节点值为3
首先从1开始,不为空也不是,找左节点;到2,也不是,找左节点;到三之后,是的,返回这个节点,可是我们并未设置一个变量来存储它的这个值,那么这个值就会丢失,如果我们使用这个函数寻找,它最后都会返回NULL

有些同学还会这样子写:

TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	return TreeFind(root->left, x) || TreeFind(root->right, x);
}

虽然这个逻辑也是对的,在左子树和右子树中寻找,但是这个||符号使得这个表达式成为了一个逻辑表达式,输出的结果只有0和1,也就是真与假,如果不找到地址,而只是看有没有这个值的节点的话,完全可以这样做,但是也需要把返回值改成bool类型

而正确的写法应该是这样的:

TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	TreeNode* ret1 = TreeFind(root->left, x);
	if (ret1)
		return ret1;

	TreeNode* ret2 = TreeFind(root->right, x);
	if (ret2)
		return ret2;

	return NULL;
}

以上图来解释一下:我们还是找3,首先从1开始,1的ret1记录2的递归结果,2的ret1记录3的递归结果;3是的,返回到2的ret1中,ret1为真,返回到1的ret1中,最后返回到main函数中并输出其地址,层层相扣。

以上就是二叉树的几种简单函数,希望大家看完后可以略懂一二!

你可能感兴趣的:(开发语言,数据结构,c语言)