二叉树链式结构的实现及应用

文章目录

前言

本文讲解了链式二叉树的前、中、后序遍历,层序遍历,并附了有关链式二叉树的一些经典问题及详解,提高大家对链式二叉树的学习深度,并学会用分治的思想解决一些问题。
建议阅读本文之前阅读一下有关二叉树的基本知识。

1. 前置说明

普通二叉树的增删查改没有价值,如果为了单纯储存数据不如使用线性表,这里的学习是为了更好的控制它的结构,为后续学习更复杂的搜素二叉树,AVL数树,红黑树等打基础,而且很多二叉树OJ算法题都出在普通二叉树上。

1.1 初建二叉树

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

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType _data;
	struct BinaryTreeNode* _left;
	struct BinaryTreeNode* _right;
}BTNode;

BTNode* BuyNode(int data)
{
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	newNode->_data = data;
	newNode->_left = newNode->_right = NULL;

	return newNode;
}
BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);

	node1->_left = node2;
	node1->_right = node4;
	node2->_left = node3;
	node4->_left = node5;
	node4->_right = node6;
	return node1;
}

二叉树[1,2,4,3,5,6]

注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。

1.2 二叉树回忆

在讲解链式二叉树之前,我们先浅浅回忆一下二叉树的概念:
二叉树是树的一种特例,该树由一个根节点加上两棵分别称为左子树和右子树的二叉树组成,两棵子树又可看成是一个根节点和属于它的两棵子树组成的二叉树,以此递归下去,直到子树为空。
二叉树的概念
这种递归的思想对于二叉树尤为重要,在之后的讲解中我你们会反复遇到类似这样的概念。

2. 二叉树的深度遍历

2.1前序、中序及后序遍历

学习二叉树的结构,最简单的方式就是遍历。所谓二叉树的遍历,就是按照某种特定的规则,依次对而二叉树中的节点进行相应的操作,并且每个节点只操作一次。 访问节点所做的操作依赖于具体的应用问题。遍历是二叉树最重要的运算之一,也是二叉树上其他运算的基础。
下图是遍历二叉树的一般递归路径,递归路径虽然相同,但由于递归过程中一个节点可能经过多次,是一进节点就访问、归回来的时候的访问、还是向上返回的时候再访问,不同的访问时序也会也会产生不同的结果
在这里插入图片描述
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:(这里的前、中、后都是说当前树的根节点进行访问的时序,且子树都是先左后右访问)

  • 前序遍历(Preorder Traversal 亦称先序遍历、先根遍历)——访问根节点的操作发生在访问左右子树之前(对应一进递归就访问)
  • 中序遍历(Inorder Traversal,亦称中根遍历)——先访问左树,再访问根节点,最后访问右树(对应归回来之后访问)
  • 后序遍历(Postorder Traversal,亦称后根遍历)——先左后右,最后访问根节点(向上返回的时候再访问)

由于真正能直接读取的只有某子树的根,所以这里的访问左子树,右子树又可解释为:访问以左节点、右节点为根的树,以此向下递归。

// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);

接下来,我们直接用打印的方式来代表节点的访问,实现一下三种遍历方式,并让大家感受一下不同结构数据的访问顺序,最后我们使用函数递归图对前序遍历进行解释,直观感受一下函数递归的过程。

2.1.1前序遍历

代码呈现:

通过定义,写出代码相对容易,但实际的递归过程却相对难理解。这里,把前序遍历的代码呈现给大家,以便后续讲解。

void PreOrder(BTNode* root)
{
	if (root == NULL);//如果树为空,打印空,不再向下
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->_data)//访问当前根节点
	PreOrder(root->_left);//访问左子树
	PreOrder(root->_right);//访问右子树
}
访问顺序:

首先,我们研究一下前序遍历到底会以什么次序进行输出(这里我们将空指针也进行输出,以便理解)

  1. 根据这个递归函数,首先输出根节点的值,然后“输出”左子树,右子树。
    前序遍历-1

  2. 分别进入左右子树后,这时我们把两棵子树看成新的树,输出这棵树的根节点值,左子树,右子树,当某一棵子树是空的话,那就输出"NULL",并不再开辟那棵树(如:2-右树)。
    前序遍历-2

  3. 2-左树打印了根节点,输出左右子树,但左右子树都为空,所以直接输出NULL,4的左树和右树方法也相同。
    前序遍历-3

最终直到所有叶(NULL)都被访问,这棵树就遍历结束了。
上图从左至右的值,就是递归过程中访问的次序。
我们看一下运行结果:
前序遍历

例题:

再上一道例题巩固一下:

如果一颗二叉树的前序遍历的结果是ABCD,则满足条件的不同的二叉树有( )种
A.13
B.14
C.15
D.16

首先思考一下,ABCD四个节点可以生成的二叉树的高度可以是多少?
高度最低也就是完全二叉树的情况,也就是高度位3;
高度最高也就是单边树(每一层只有一个节点)的情况,高度位4;
接下来就分别对这两种情况进行分析:

  • 高度位3:
    这时,我们就可以先把一个三层树对应的输出框架先画出来:
    三层满二叉树
    这时向框内填入所有情况:
    (由于头节点一定会有值,则且一定是A,所以直接放入)在这里插入图片描述
    共6种情况。
  • 高度为4:
    这时一定有从第二层开始都有左右两种可能,所以一共有2^3 = 8种可能。
    在这里插入图片描述

上面两种情况一共是6+8 = 14种组合,选B。

递归过程

接下来我们来一步步分析一下递归的过程
前序遍历递归图解

二叉树前序遍历递归图

这就是一段完整的前序遍历的递归过程

2.1.2中序遍历&后后序遍历

有了前序遍历的讲解,中序遍历和后序遍历也基本相同,这里我们就简单展示一下访问的顺序和代码,至于递归过程,基本与前序相同,仅仅是访问根节点数据区的时间不同,大家可以自己画图感受一下。

代码——中序遍历
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);
}
后序遍历——访问顺序

后序遍历——访问顺序
后序遍历——运行结果:
后序遍历运行结果

中序遍历——访问顺序

最后留一个中序遍历的访问顺序图给大家自己实现以下👻,可以对照以下运行结果哦。
中序遍历运行结果

2.1.3 综合例题

  1. 已知某二叉树的中序遍历序列为JGDHKBAELIMCF,后序遍历序列为JGKHDBLMIEFCA,则其前序遍历序列为( )
    A.ABDGHJKCEFILM
    B.ABDGJHKCEILMF
    C.ABDHKGJCEILMF
    D.ABDGJHKCEIMLF

解析:

  • 这里要求出前序遍历序列,就需要先画出整棵二叉树
  • 根据后序遍历序列,可以确定根节点,最后一个节点即根节点
  • 根据中序遍历和一个根节点可以确定出左右子树的所有元素,根节点左侧即左树元素,根节点右侧即右树元素
  1. 通过后序遍历序列:JGKHDBLMIEFCA,可知A为根节点;
  2. 通过中序遍历序列:JGDHKBAELIMCF,可知左树元素有JGDHKB,右树元素有ELIMCF
  3. 左树元素在后序遍历序列中的顺序为LMIEFC,左树的根节点就是就是C;右树逻辑形同
  4. 再通过中序遍历序列找到子树的左右子树,以此类推……直至推完所以元素

根为: A

A的左子树:JGDHKB ; A的右子树:ELIMCF

A的左子树的根:B ; A的右子树的根:C

B的左子树:JGDHK ; B的右子树:空;; C的左子树:ELIM ;C的右子树:F

B的左子树的根:D ; C的左子树根:E

D的左子树的根:G ;D的右子树的根:H ;E的右子树的根:I

故树的结构为:

           A
           
  B                  C
  
    D                  E       F
    
     G  H                  I
     
     J     K               L  M

故前序遍历:

A B D G J H K C E I L M F

2.已知某二叉树的前序遍历序列为ABDEC,中序遍历序列为BDEAC,则该二叉树( )
A.是满二叉树
B.是完全二叉树,不是满二叉树
C.不是完全二叉树
D.是所有的结点都没有右子树的二叉树

与上一题大致相同,只不过这回前序遍历的首元素是根节点
相同思路可以确定树的结构:

   A

 B     C

  D

    E

所以既不是满二叉树,也不是完全二叉树

2.2节点个数及高度

之前的前中后序都是对节点的访问,这里,我们依然是使用这三种方式进行节点个数的计算,并逐步理解分治的思想。

// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
//二叉树最大深度
int BinaryTreeDepth(BTNode* root);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);

2.2.1二叉树节点个数

方式1——计数器版

我们首先设置一个计数器,从三种遍历方式中随机选一种,把之前的访问化成给计数器++,遍历完整棵数之后也就获取了节点个数

代码如下:
void BinaryTreeSize(BTNode* root,int* counter)
{
	if (root == NULL)//空树返回大小为0
	{
		return ;
	}
	(*counter)++;
	BinaryTreeSize(root->left, counter);
	BinaryTreeSize(root->right, counter);
}

这里有个小问题:这里的counter可以是int型吗?
答案是不可以,如果是int型,那么传值传的counter都会有一个临时拷贝,给这个临时拷贝++后,实际的counter值并未改变,所以必须使用传址的方式。

方式2——分治版

在讲解方法2之前,我们先想象一个场景:

一天校长想统计这个学校一共有多少人,这时,他把所有的院长都叫过来,让他们上报自己学院有多少人,院长下去了又把所有辅导员叫过来询问,辅导员又叫来了所有班长,班长回去又叫了各个社长,社长统计后上报班长,班长上报辅导员,层层上报,最终校长就知道这个学校有多少人了。

其实数二叉树节点个数就是这个原理:
头节点获得左右子树的节点个数,再加上自己,就是整个二叉树的大小了。这里的子树也是相同的获取原则,直至出现一颗空树,空树就上报0.

代码如下:
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)//空树返回大小为0
	{
		return 0;
	}
	//返回左子树大小+右子树大小+自己1
	return BinaryTreeSize(root->left) + 
	BinaryTreeSize(root->right) + 1;
}

由于版本1的参数更多,操作更复杂,使用方式二更易理解,所以建议使用方式二

2.2.2叶子节点个数

所谓叶子节点就是含有子树个数为0的节点,也就是左节点和右节点都是NULL。与计算总节点的个数相似,只不过这里又有遇到子节点才+1.同样这里也可以用计数器和分治两种方式解决,但我只展示以下分治的方式,计数器大家如果感兴趣可以自己实现。

int BTreeLeafSize(BTNode* root)
{
	//空节点
	if (root == NULL)
	{
		return 0;
	}
	//叶子节点
	if (root->_left == NULL && root->_right == NULL)
	{
		return 1;
	}
	//返回左右子树的叶子节点个数
	return BTreeLeafSize(root->_right) + BTreeLeafSize(root->_left);
}

2.2.3第K层的节点个数

返回第K层的节点个数,可以看成是,返回两个子树的”K-1层的节点个数“,子树的子树又是”K-2层的节点个数“以这种方式递归下去,一直到"返回第1层的节点的个数",这时返回1就好,因为任何二叉树的第一层只有一个节点,当然如果在k减到1之前,提前递归到了NULL节点,那返回0即可,表示这个分支下没有第K层节点呗。

int BinaryTreeLeafSize(BTNode* root, int k)
{
	assert(k >= 1);
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)//递归到这棵树是,刚好是求第一层的节点数
	{
		return 1;
	}
	//递归到子树就是求k-1层的节点数,左子树加右子树就是总共的第k层的节点数
	return BinaryTreeLeafSize(root->_left, k - 1) + BinaryTreeLeafSize(root->_right, k - 1);
}

2.2.4二叉树的最大深度

二叉树
这样一棵二叉树的最大深度就是4,即ABEH这一条线路最长
这样一个问题可以归结成是:比出左右子树中深度更大的那个的深度,加自己的1深度就是总深度
总结成分支问题就是:返回左右子树深度较大的+1;
同样遇到空数返回深度为0;

int BTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int leftDepth = BTreeDepth(root->_left);
	int rightDepth = BTreeDepth(root->_right);
	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}

2.2.5二叉树查找值为x的节点

在二叉树查找值为x的节点,返回次节点的地址,若找不到返回NULL

查找过程无非就是在遍历过程中寻找值为x的节点,找到就返回这个节点的地址,至于前中后序都可以

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->val == x)//找到就返回
	{
		return root;
	}
	BTNode* leftTree = BinaryTreeFind(root->left, x);//左树是否找到
	if (leftTree != NULL)//找到了返回找到的节点
	{
		return leftTree;
	}
	BTNode* rightTree = BinaryTreeFind(root->right, x);//右树是否找到
	if (rightTree != NULL)//找到了返回找到的节点
	{
		return rightTree;
	}
	return NULL;//当前节点、左树、右树都没找到就返回空
}

2.3二叉树基础oj练习

1.单值二叉树

如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。
只有给定的树是单值二叉树时,才返回 true;否则返回 false。

方法一:带值比较法

由于oj给定的函数是只有一个根节点的,但我们这里需要一个参数放头节点的值以便后序所有节点去和他比较,所以设置一个辅助函数,让原函数去调用。像这样:

bool _isUnivalTree(struct TreeNode* root,int val)
{
}
bool isUnivalTree(struct TreeNode* root)
{
	return _isUnivalTree(root, root->val);
}

有了这个比较值就相对好写:
返回”根节点==val&&左树是单值数&&右树也是单值数“即可

bool _isUnivalTree(struct TreeNode* root,int val)
{
	if (root == NULL)
	{
		return true;
	}
	if (root->val != val)
	{
		return false;
	}
	return _isUnivalTree(root->left,val) && _isUnivalTree(root->right,val);
}
bool isUnivalTree(struct TreeNode* root)
{
	return _isUnivalTree(root, root->val);
}
方法二:左右比较法

分析:
如果要带比较值,还要写辅助函数,相对麻烦,我们是不是可以直接让根节点和左右进行比较。

  • 同样使用分治的方法,如果左树和右树都是单值二叉树,返回真
  • 如果根节点不等于左节点或右节点就返回假
  • 遇到空节点返回真
bool isUnivalTree(struct TreeNode* root)
{
	if (root == NULL)
	{
		return true;
	}
	if (root->left && root->left->val != root->val)
	{
		return false;
	}
	if (root->right && root->right->val != root->val)
	{
		return false;
	}
	return isUnivalTree(root->left) && isUnivalTree(root->right);
}

2.检查两棵树是否相同

相同的树

这道题用分治的思想也很好解决。

  • 两棵树相同,无非就是:根节点相同,两棵树的左树相同,两棵树的右树相同就返回真。

  • 宏观思想解决了,那我们来看看递归的终止条件,盲猜递归到空节点就返回,我们分析一下细节:

    1.递归到左树右树都为真,无疑返回真
    2.要是只有一棵树为空,节点数都不同,两棵树一定不同,返回假

看一下代码:

bool isSameTree(struct TreeNode* p, struct TreeNode* q) 
{
	if (p || q)//不全是空
	{
		if (p && q)//都不空
		{
			return (p->val == q->val) && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
		}
		else//一棵空一棵不空
		{
			return 0;
		}
	}
	else//全空
	{
		return 1;
	}
}

3. 对称二叉树

对称二叉树
要求判断是否为对称二叉树,根节点毫无作用,唯一需要判断的就是左右子树是否对称或者说镜像。
为了递归方便,我们干脆定义一个辅助函数,传入左树和右树

bool treeSymmCmp(TreeNode* p, TreeNode* q)
{
}
bool isSymmetric(struct TreeNode* root)
{
	struct TreeNode* treeLeft = root->left;//左子树
	struct TreeNode* treeRight = root->right;//右子树
	return treeSymmCmp(treeLeft, treeRight);//调用辅助函数
}

接下来研究一下这个辅助函数怎么写:
与上一题相似,判断p、q两棵树是否对称不可以看成:

  • 判断p、q根节点是否相等,
  • 判断p的右树和q的左树是否对称
  • 判断p的左树和p的右树是否对称

如果还不能确保,不妨试一下子问题是否还符合这样的判断方式:
如下这棵对称树
对称二叉树
摘取左右树:
对称二叉树——左右子树

  • . 第一层递归
    • 1树和2树的根节点相同
    • 1的左子树3和2的右子树6对称
    • 1的右子树4和2的左子树5对称
  • 第二层递归
    • 3的根节点和6的根节点相同
    • 3的左子树7和6的右子树8对称
    • 3的右子树和6的左子树对称

我们发现,第二层递归与第一层递归的判断方式可以完全重叠,那么宏观的递归逻辑就对了
接下来看看微观的结束条件:
看第二层的第3行可知,两树都为空就返回真,
若两棵树只有一颗不是空,那么返回假(如果4不为空,5为空,那么一定不对称)
我们实现一下:

bool treeSymmCmp(TreeNode* p, TreeNode* q)
{
	if (p || q)
	{
		if (p && q)
		{
			return (p->val == q->val) && treeSymmCmp(p->left, q->right) && treeSymmCmp(p->right, q->left);
		}
		else
		{
			return 0;
		}
	}
	else
	{
		return 1;
	}
}
bool isSymmetric(struct TreeNode* root)
{
	struct TreeNode* treeLeft = root->left;
	struct TreeNode* treeRight = root->right;
	return treeSymmCmp(treeLeft, treeRight);
}

4.前序遍历——数组返回

与之前写的遍历基本相同,只不过这里不是打印,而是拷贝到数组中,然后返回整个数组。

  • 同样,这里题目给的函数是不利于递归的,我们依然构造一个辅助函数。
  • 由于创建数组还需要节点的个数,所以还要调用之前的BTreeSize()算一下节点个数。
    代码呈现:
int BTreeSize(struct TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return BTreeSize(root->left) + BTreeSize(root->right) + 1;
}
void PrevOrder(struct TreeNode* root, int* arr, int* num)
{
	if (root == NULL)
	{
		return;
	}
	arr[*num] = root->val;
	++(*num);
	PrevOrder(root->left, arr, num);
	PrevOrder(root->right, arr, num);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) 
{
	*returnSize = BTreeSize(root);
	int* arr = (int*)malloc(sizeof(int) * *returnSize);
	int size = 0;
	PrevOrder(root, arr, &size);
	return arr;
}

5.二叉树中序遍历&&后序遍历

二叉树的中序遍历
二叉树的后序遍历
与前序基本相似,这里直接上答案:

中序:
int TreeSize(struct TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return TreeSize(root->left) + TreeSize(root->right)+1;
}

void _inorderTraversal(struct TreeNode* root, int* a, int* i)
{
	if (root == NULL)
	{
		return;
	}
	_inorderTraversal(root->left, a, i);
     a[*i] = root->val;
	(*i)++;
	_inorderTraversal(root->right, a, i);
   
}

int* inorderTraversal(struct TreeNode* root, int* returnSize) 
{
	int treeSize = TreeSize(root);
	int* ret = (int*)malloc(sizeof(int) * treeSize);
	int i = 0;
	_inorderTraversal(root, ret, &i);
	*returnSize = treeSize;
	return ret;
}
后序:
int TreeSize(struct TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return TreeSize(root->left) + TreeSize(root->right)+1;
}

void _postorderTraversal(struct TreeNode* root, int* a, int* i)
{
	if (root == NULL)
	{
		return;
	}
	_postorderTraversal(root->left, a, i);
	_postorderTraversal(root->right, a, i);
    a[*i] = root->val;
	(*i)++;
}

int* postorderTraversal(struct TreeNode* root, int* returnSize) 
{
	int treeSize = TreeSize(root);
	int* ret = (int*)malloc(sizeof(int) * treeSize);
	int i = 0;
	_postorderTraversal(root, ret, &i);
	*returnSize = treeSize;
	return ret;
}

6. 另一棵树的子树

另一棵树的子树
这里放这道题,仅仅是为了熟悉链式二叉树的数据结构,这里就先没必要涉及一些优化的算法,有兴趣可以看一下LeeTcode官方的讲解,我们这里讲解一下暴力算法就好。

方法一:
  • 判断是否存在子树,是否相当于遍历root的每一个节点,检查root中是否有一棵树与subtree完全相同,这又回到了检查两棵树是否相同的问题。
  • 再来控制一下结束条件:
    • 一直遍历到空,就返回假
    • 找到相同的树了就返回真
    • 任意一边返回真了就是真

代码:

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
	if (p || q)
	{
		if (p && q)
		{
			return (p->val == q->val) && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
		}
		else
		{
			return 0;
		}
	}
	else
	{
		return 1;
	}
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
	if (root == NULL)//探到底都没有,返回假
	{
		return false;
	}
	if (isSameTree(root, subRoot))//前序遍历是否有相同
	{
		return true;
	}
	bool judLeft = isSubtree(root->left, subRoot);//判断左树
	bool judRight = isSubtree(root->right, subRoot);//判断右树
	return judLeft || judRight;//若左右任意一棵树是,也返回真
}
方法二:
  • 还是使用分治的方法,如果当前树与子树相同、左树中有子树、右树中有子树,这三个条件任意一个成立,就返回真
  • 递归结束条件:
    • 走到空

代码:

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
	if (p || q)
	{
		if (p && q)
		{
			return (p->val == q->val) && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
		}
		else
		{
			return false;
		}
	}
	else
	{
		return true;
	}
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
	
	if (root == NULL)
	{
		return false;
	}
	else
	{
		return isSameTree(root,subRoot) || isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
	}
}

3. 二叉树的广度遍历

3.1 前言

前面的前、中、后序遍历,包括所有的例题属于深度有限遍历,通过递归的方式,按照箭头顺序,经历如下的遍历。
在这里插入图片描述
这里我们再来看一下二叉树的层序遍历,即如下一层一层的遍历方式:
层序遍历
在这里插入图片描述

3.2 原理

如何实现呢?
当走到一个节点,只能访问到三部分,当前节点,左树,右树
但也不可能左右同时进行,所以必须对节点进行存储,访问存好的节点:

  • 储存A节点
  • 访问存好的A;存A的左右:B、C
  • 访问存好的B、C;存B、C的左右D、E、F、G
  • 访问D、E、F、G;存……

以此类推,直至访问到空
我们可以看到存储过程中,进入的顺序是A、B、C、D、E……,访问的顺序也是A、B、C、D、E……,也就是先进先出,这时大家想到了哪种数据结构?
诶,对!
队列
所以我们使用队列的结构来辅助实现,大家可以从如下的链接中找到链表的结构和c语言实现,下面会直接调用
数据结构——队列

3.3 实现思路

观察上述原理中存储和访问的过程中,只有第一行是只进行了存储的,后面都是先访问,再存子节点,所以毫无疑问,除了第一句,剩余循环实现就好。
接下来再思考这样一个问题:有必要一次把一层都遍历完吗?
不妨试一试每次只对一个节点进行访问,同时带入它的两个子节点:

3.3.1 宏观访问

  • 取出A进行访问,带入B,C
    二叉树层序遍历入队列
  • 取出B进行访问,带入D、E
    二叉树层序遍历入队列
  • 取出C进行访问,带入F、G
    二叉树层序遍历入队列
    我们发现,即使每次仅进行一个节点的访问,依然可以正常的层序遍历。

3.3.2 微观结束条件

循环什么时候结束呢?
最一开始,队列就有一个元素(根节点),之后只要访问的节点还有子节点,那队列就一直有元素输入,直至队列中所有元素都没了子节点,再把队列访问完,也就结束了整个层序遍历过程。
二叉树层序遍历入队列——结束条件
所以结束条件就是队列为空。

3.4 代码

void LevelOrder(BTNode* root)
{
	//创建队列
	Queue q;
	QueueInit(&q);
	if (root)//避免传入了空树
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))//结束条件——队列为空
	{
		BTNode* ret = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", ret->data);//访问
		if (ret->left)//有左节点,入队列
		{
			QueuePush(&q, ret->left);
		}
		if (ret->right)//有右节点,入队列
		{
			QueuePush(&q, ret->right);
		}
	}
	QueueDestroy(&q);//销毁队列
}

3.5 判断二叉树是否为完全二叉树

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root);

分析

![完全二叉树——一般二叉树](https://img-blog.csdnimg.cn/98816bf27bc94ec3ac5d2e24efa2b9d3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWFzc2FjaHVzZXR0c18xMQ==,size_16,color_FFFFFF,t_70,g_se,x_16🙃🙃🙃🙃🙃层序遍历完全二叉树
在这里插入图片描述

观察上图,我们发现,所谓完全二叉树,无非就是在整段层序遍历过程,不在中间出现任意一个空隙。
那么,怎么实现呢?

  • 首先对二叉树进行层序遍历
  • 在访问队列元素过程中,当出现任意一个空节点,那么访问队列中剩余的所有元素,一旦出现一个非空的节点,那么它就不是完全二叉树。
  • 注意这里元素进队列的过程中如果子节点是空,不可像之前的层序遍历一样,直接跳过元素,还需在对列中进行的标记。

这里我们讨论一个问题:
有没有这种可能:在出现空节点时,右侧的非空节点还没来的及入队列,导致无法正确判断出结果?
答案是不可能的,观察上面的一般二叉树,当层序遍历到空节点时,则上一层的节点一定是全部访问,访问的同时他们也将下一层的全部节点都带出来了,所以一定不可能出现这种情况。

实现

bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root==NULL)//空树返回假
	{
		return false;
	}
	QueuePush(&q, root);//根节点入队列
	BTNode* nodeInQ = root;
	while (nodeInQ)//循环找到队列中的空节点
	{
		QueuePush(&q, nodeInQ->left);
		QueuePush(&q, nodeInQ->right);
		QueuePop(&q);
		nodeInQ = QueueFront(&q);
	}
	while (!QueueEmpty(&q))//遍历剩余队列
	{
		nodeInQ = QueueFront(&q);
		QueuePop(&q);
		if (nodeInQ)//再次出现非空,不是完全二叉树
		{
			return false;
		}
	}
	QueueDestroy(&q);
	return true;//未出现非空,就是完全二叉树
}

4.二叉树的创建与销毁

4.1二叉树的构建及遍历

描述:

编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

分析:

题目中给出的“ABC##DE#G##F###”这个字符串实际上就是对一个二叉树进行前序遍历后的输出结果,我们需要凭借这个字符串,将二叉树还原出来,然后再对还原出来的二叉树进行中序遍历输出。至于输出直接调用前面的代码就好,我们这里主要分析一下它的创建过程。
如何前序创建一棵树呢?
在讲解创建之前,先让我们回忆一下先前访问的前序遍历

  • 访问当前节点
  • 遍历左树
  • 遍历右树
  • 遇到空节点直接返回

这时,我们仿照访问的方式,写一个创建试试

  • 创建当前节点
  • 创建左树,连接到当前节点左侧
  • 创建右树,连接到当前节点右侧
  • 遇到“#”,返回一棵空树,也就是返回一个空节点,NULL

函数怎么构造?

  • 传参:
    1. 存节点值的字符串
    2. 为了记忆当前字符串访问到了那个节点,所以还要有一个参数记录当前创建到几个节点了(数组下标),所以一共需要三个参数
  • 返回值:返回树的根节点

BTNode* BinaryTreeCreate(BTDataType* a, int* i);
//a——字符串,n——字符串长度,pi——当前字符下标

注意:这里的 i 必须是指针。如果传int,那么 i 就是个临时变量,生命周期只在当前函数,也就是说对 i 的修改只在当前函数有效,当递归返回时,这一层函数的 i还是指向前面位置的;如果使用指针,则每次修改的就都是同一个 i 了。

实现

//二叉树结构
typedef char BTDataType;
typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	BTDataType data;
}BTNode;

//创建二叉树
BTNode* CreatBinaryTree(char* str,int* i)
{
	if (str[*i] == '#')
	{
		(*i)++;
		return NULL;
	}
	if (str[*i] == '\0')
	{
		return NULL;
	}
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	newNode->data = str[*i];
	(*i)++;
	newNode->left = newNode->right = NULL;
	newNode->left = CreatBinaryTree(str, i);
	newNode->right = CreatBinaryTree(str, i);
	return newNode;
}

//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}

//测试函数
int main()
{
	char str[100] = { 0 };
	scanf("%s", str);
	int i = 0;
	BTNode* tree = CreatBinaryTree(str, &i);
	InOrder(tree);
}

4.2二叉树的销毁

void BinaryTreeDestory(BTNode** root);

思路

销毁二叉树,也就是把每个节点都free掉,无非就是遍历一下,只不过这里唯一要注意的一点就是:一定要把左右树都销毁了,再销毁根节点,否则先销毁了根就找不到左右树了。

实现

void BinaryTreeDestory(BTNode** root)//这里传一级也可以,只不过将root置空一下保险一点
{
	if (*root == NULL)
	{
		return;
	}
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);
	free(*root);
	*root = NULL;
}

笔者书写不易,希望大家点赞支持一下哦,当然三连就更好啦😘
大家一起努力哇!!!!

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
树是一种常见的数据结构,它由一个根节点和每个节点最多有两个子节点组成。这两个子节点分别称为左子节点和右子节点。二树的存储结构一般有两种:链存储和顺序存储。 链存储是指每个节点都保存了指向它的左子节点和右子节点的指针。这种存储方适合于动态结构,因为它可以动态地分配内存。链存储的二树可以用如下的 C 语言代码来实现: ``` typedef struct TreeNode { int value; struct TreeNode *left; struct TreeNode *right; } TreeNode; ``` 顺序存储是指将二树的节点按照某种顺序依次存储下来,从而形成一个数组。对于一个节点的索引为 i,它的左子节点的索引为 2*i,右子节点的索引为 2*i+1。这种存储方适合于静态结构,因为它不需要动态地分配内存。顺序存储的二树可以用如下的 C 语言代码来实现: ``` #define MAX_SIZE 100 int tree[MAX_SIZE]; void set_node(int index, int value) { tree[index] = value; } int get_node(int index) { return tree[index]; } int get_left_child(int index) { return tree[2*index]; } int get_right_child(int index) { return tree[2*index+1]; } ``` 二树在计算机科学中广泛应用,它是很多算法和数据结构的基础。例如,二搜索树是一种特殊的二树,它的每个节点的左子树中的所有节点都小于它,右子树中的所有节点都大于它。二搜索树可以用于实现快速查找和排序。还有其他类型的二树,例如红黑树、AVL 树、堆等等,它们都有各自的特点和应用场景。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值