【数据结构】第八节:链式二叉树

个人主页: NiKo

数据结构专栏: 数据结构与算法

源码获取:Gitee------数据结构

一、二叉树的链式结构

cpp 复制代码
typedef int BTDataType;
typedef struct BinaryTreeNode {
	BTDataType data;
	struct BinaryTreeNode* left;  // 左子树根节点
	struct BinaryTreeNode* right; // 右子树根节点
}BTNode;

每一颗二叉树都是由左子树、根、右子树构成的,在实现二叉树的链式结构时我们也要将二叉树看作这三部分。

二、二叉树的遍历

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

  1. 前序遍历(Preorder Traversal 亦称先序遍历)------访问根结点的操作发生在遍历其左右子树之前。
  2. 中序遍历(Inorder Traversal)------访问根结点的操作发生在遍历其左右子树之中(间)。
  3. 后序遍历(Postorder Traversal)------访问根结点的操作发生在遍历其左右子树之后。

图1-1

已经创建的二叉树如图1-1。

1.前序遍历

前序遍历访问根结点的操作发生在遍历其左右子树之前,访问顺序为根、左子树、右子树。根据概念,在访问二叉树时,需要将二叉树看作根、左子树、右子树,左右子树又可以分为根、左子树、右子树,直到访问的节点为空(NULL)即证明访问到了二叉树的最底层。

如图1-1所示,首先访问的根是1,这是第一层 递归,然后递归访问此根的左子树,根节点为2,这是第二层 递归;而以2为根的子树又可以分为以3为根的左子树、NULL,访问完根节点2后应继续递归访问左子树根节点3,这是第三层 递归;此时根节点3的左子树为空,右子树为空无法再递归下去,以3为根节点的子树访问完毕,结束第三层递 归返回到第二层递归并访问根节点2的右子树,右子树为空无法递归,以2为根节点的子树访问完毕结束第二层递归返回到第一层递归并访问根节点1的右子树,同理根据遍历左子树的方式遍历右子树。

cpp 复制代码
void PrevOrder(BTNode* root) {
    // 不符合递归条件,结束此层递归
	if (root == NULL) {
		printf("N ");
		return;
	}
	printf("%d ", root->data); // 访问根节点
	PrevOrder(root->left);     // 访问根节点的左子树
	PrevOrder(root->right);    // 访问根节点的右子树
}

根据代码前序遍历的结果示意图:


2.中序遍历

中序遍历访问根结点的操作发生在遍历其左右子树之中(间)。访问顺序为左子树、根、右子树。同前序遍历的分析方式一样,访问一棵树应从他的左子树开始访问,然后再访问根和右子树。

如图1-1,首先访问以1为根的左子树(根节点2),进入第一层递归 ;以根节点为2的左子树又可以分为左子树(根节点3)、右子树(NULL),进入第二层递归 ;此时正在访问的是根节点为3的子树,根据中序遍历的规则应按照左子树、根、右子树的顺序访问,进入第三层递归 。3的左右子树都为空不符合递归条件结束第三层递归 ,返回到第二层递归并访问根节点2,然后访问根节点2的右子树,右子树为空结束第二层递归,返回到第一层递归并访问根节点1,随后访问根节点1的右子树,同理根据遍历左子树的方式遍历右子树。

cpp 复制代码
void InOrder(BTNode* root) {
	if (root == NULL) {
		printf("N ");
		return;
	}
	InOrder(root->left);        // 访问根的左子树
	printf("%d ", root->data);  // 访问根
	InOrder(root->right);       // 访问根的右子树
}

根据代码中序遍历的结果示意图:


3.后序遍历

后序遍历访问根结点的操作发生在遍历其左右子树之后。访问顺序为左子树、右子树、根,后序遍历访问一棵树时,根节点是最后访问的。

如图1-1,首先访问根节点1的左子树(根节点2)进入第一层递归 ,这颗子树还可以分为左右子树,则进入第二层递归 访问根节点2的左右子树。随后进入第三层递归 先访问根节点3的左子树,然后是右子树,最后是根节点3,结束第三层递归 返回第二层递归并访问根节点2的右子树之后在访问根节点2,结束第二层递归返回第一层递归,访问根节点1的右子树,最后访问根节点1。

cpp 复制代码
void PostOrder(BTNode* root) {
	if (root == NULL) {
		printf("N ");
		return;
	}
	PostOrder(root->left);      // 访问左子树
	PostOrder(root->right);     // 访问右子树
	printf("%d ", root->data);  // 访问根
}

根据代码后序遍历的结果示意图:

三、节点个数


  • 对于一棵树而言,他的节点分为两种情况:
  1. 根节点为空,返回0。
  2. 根节点不为空,这颗树的节点数=左子树节点个数+右子树节点个数+根节点(1)。
  • 根据这两种情况写出的代码如下:
cpp 复制代码
int TreeSize(BTNode* root) {
	return root == NULL ? 0 :
		TreeSize(root->left) + TreeSize(root->right) + 1;
}

  • 分析:第一个访问的根节点1不为空根据表达式先计算左子树的节点个数,通过TreeSize(root->left)进入第一层递归 访问到根节点2,在第二层递归中通过TreeSize(root->left)进入第二层递归 访问根节点3,根节点3不为空通过TreeSize(root->left)访问左子树进入第三层递归,左子树为空返回0,右子树为空返回0,第三层递归的返回值为0+0+1=1结束第三层递归, 返回到第二层递归的TreeSize(root->left)的值就是1,然后在第二层递归中通过TreeSize(root->right)访问根节点2的右子树,右子树为空返回0,第二层递归的返回值1+0+1=2,结束第二层递归并将值返回到第一层递归的TreeSize(root->left),这样左子树的节点个数计算完毕为2,紧接着再计算第一层递归的TreeSize(root->right),同理可得TreeSize(root->right)的值为3,最后得出这棵树的节点个数是6。

四、叶子节点个数

叶子节点是指不含有任何子树的节点(度为0),即root->left和root->right均为NULL。判断叶子节点的个数需要我们找到度为0的节点。


  • 对于一颗子树,它的叶子节点有三种情况:
  1. 根节点为空,返回0
  2. 根节点不为空,左右子树均为空,它本身就是叶子节点,返回1
  3. 根节点不为空,左右子树至少有一个不为空,这棵树的叶子节点树=左子树叶子节点数+右子树叶子节点数
  • 根据这三种情况写出的代码如下:
cpp 复制代码
int TreeLeafSize(BTNode* root) {
	if (root == NULL) {
		return 0;
	}
	else if (root->left == NULL && root->right == NULL) {
		return 1;
	}
	else {
		return TreeLeafSize(root->left) + TreeLeafSize(root->right);
	}
}

  • 分析:进入第一层递归 访问根节点1,根节点1不为空且左右子树不为空,通过TreeLeafSize(root->left)进入第二层递归 访问根节点2,根节点2不为空且左子树不为空,通过TreeLeafSize(root->left)进入第三层递归 访问根节点3,根节点3不为空但是此时左右子树为空,说明节点3是叶子节点,结束第三层递归 返回1到第二层递归,在第二层递归中通过TreeLeafSize(root->right)访问根节点2的右子树,为空返回0,则以2为根节点的子树叶子节点个数0+1=1,结束第二层递归返回1到第一层递归,同理可得右子树的叶子节点为1+1=2,则这棵树的叶子节点数1+2=3。

五、高度

树的高度是指树中节点的最大层次,在二叉树中,左右两棵子树的高度较大者作为这棵树的高度。


  • 求树的高度有两种情况:
  1. 根节点为空,返回0
  2. 根节点不为空,这棵树的高度=max(左子树高度,右子树高度)+1
  • 根据这两种情况写出的代码如下:
cpp 复制代码
int TreeHeight(BTNode* root) {
	if (root == NULL) {
		return 0;
	}
	else {
		return max(TreeHeight(root->left), TreeHeight(root->right)) + 1;
	}
}

  • 分析:进入第一层递归 访问根节点1,不为空通过TreeHeight(root->left)进入第二层递归 访问根节点2,不为空通过TreeHeight(root->left)访问根节点3进入第三层递归 ,此时TreeHeight(root->left)和TreeHeight(root->right)的返回值都为0,第三层递归返回0+0+1=1到第二层递归并结束第三层递归。第二层递归返回1+0+1=2到第一层递归,同理再访问右子树得TreeHeight(root->right)的返回值为2,max(TreeHeight(root->left),TreeHeight(root->right))+1的结果为3,故此树的高度就是3。

六、第k层的节点个数

求二叉树的第k层节点可以向下转化为求这棵树的左右子树的第k-1层节点,当k=1时就到达了这棵树的第k层。


  • 在二叉树中每次向下递归一层的情况有三种:
  1. 根节点为空,返回0
  2. 根节点不为空且k不等于1,继续向下递归直到k等于1
  3. 根节点不为空且k等于1,说明到达目标层,返回1
  • 根据这三种情况写出的代码如下:
cpp 复制代码
int TreeLevelKSize(BTNode* root, int k) {
	if (root == NULL) {
		return 0;
	}

	if (k == 1) {
		return 1;
	}
	else {
		return TreeLevelKSize(root->left, k - 1) + 
            TreeLevelKSize(root->right, k - 1);
	}
}

  • 分析:假设k=3,进入第一层递归 访问根节点1,不为空且k不等于1(k=3);通过TreeLevelKSize(root->left, k - 1)进入第二层递归 访问根节点2,不为空且k不等于1(k=2);通过TreeLevelKSize(root->left, k - 1)进入第三层递归访问根节点3,不为空,此时k=1说明到达目标层,返回1结束第三层递归。在第二层递归中通过TreeLevelKSize(root->right, k - 1)访问右子树,为空返回0,结束第二层递归返回1到第一层递归,同理通过TreeLevelKSize(root->right, k - 1)可到达根节点1的右子树且第k层节点数是2,所以这棵树的第三层有1+2=3个节点。

七、查找值为x的节点

遍历二叉树找到值为x的节点返回即可。


  • 遍历的结果有四种情况:
  1. 根节点为空,返回NULL
  2. 根节点不为空,节点的值等于x,返回这个节点(地址)
  3. 如果在左子树中找到了目标节点,不用再遍历右子树
  4. 左右子树都没有找到这个节点,返回NULL
  • 根据这四种情况写出的代码如下:
cpp 复制代码
BTNode* TreeFind(BTNode* root, BTDataType x) {
	if (root == NULL) {
		return NULL;
	}

	if (root->data == x) {
		return root;
	}
	BTNode* ptr = TreeFind(root->left, x);
	if (ptr != NULL) {
		return ptr;
	}
	else {
		BTNode* ptr = TreeFind(root->right, x);
		if (ptr == NULL) {
			return NULL;
		}
		else {
			return ptr;
		}
	}
}

  • 分析:假设x=6,进入第一层递归 访问根节点1,1不等于6且不为空通过TreeFind(root->left, x)进入第二层递归 访问根节点2,2不等于6且不为空进入第三层递归 访问根节点3,3不等于6,此时结束第三层递归 返回值NULL,根节点2的左子树没有找到,向右子树遍历,右子树也没有找到结束第二层递归返回NULL,说明在左子树中没有找到目标节点,这时候查找右子树,在右子树中找到了就返回这个节点。

八、创建二叉树

已知一个字符数组,根据这个字符数组创建二叉树;创建二叉树的顺序应该是根、左子树、右子树。


  • 假设给出这样一个数组:"abc##de#g##f###",其中"#"表示NULL,字母代表树中节点存储的值,还是用递归的思想,根据这个数组建立一个二叉树。
  • 首先需要遍历这个数组,遍历的结果有两种:
  1. 指针指向的元素为"#",代表这个节点为NULL(左子树或右子树建立完成)
  2. 指针指向非"#"元素,将值赋给这个节点后继续建立这个节点的左子树和右子树
  • 根据这两种情况写出的代码如下:
cpp 复制代码
// abc##de#g##f###
BTNode* CreateTree(BTDataType* arr, int* pi) {
	if (arr[*pi] == '#') {
		(*pi)++;
		return NULL;
	}
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL) {
		perror("malloc fail!");
		exit(-1);
	}
	node->data = arr[(*pi)++];
	node->left = CreateTree(arr, pi);
	node->right = CreateTree(arr, pi);
	return node;
}

  • 分析:定义一个指针,代表数组的下标,每当创建一个节点或遇到NULL就后移。创建好一个节点后,给节点赋值,然后进入递归,当函数返回NULL时说明这颗子树创建完成,以此类推。

九、销毁二叉树

销毁二叉树时,采用后序遍历销毁节点,原因是如果采用前序遍历或中序遍历会导致根节点销毁后无法销毁他的左子树或右子树。


  • 销毁二叉树步骤:
  1. 如果当前节点为NULL,直接结束
  2. 不为空,先销毁左子树,再销毁右子树,最后销毁根
  • 代码如下:
cpp 复制代码
void TreeDestory(BTNode* root) {
	if (root == NULL) {
		return;
	}

	// 采用后序遍历销毁树
	TreeDestory(root->left);
	TreeDestory(root->right);
	free(root);
}

  • 分析:进入函数后先判断这个节点是不是空,如果是代表已经销毁,不是通过递归依次销毁左子树和右子树,最后销毁根。

十、层序遍历

层序遍历(广度优先遍历)指的是在一棵二叉树中,依次遍历访问每一层的节点,直到最后一层。层序遍历需要使用到队列结构,相关代码在gitte中。


  • 层序遍历基本步骤:
  1. 如果节点为空,不进队列;节点不为空,进队列
  2. 进入循环,取队列的头节点,头节点的左右节点(非空)入队列,头节点出队列,这时访问了一层节点
  3. 当队列为空时,结束循环
  • 代码如下:
cpp 复制代码
void TreeLevelOrder(BTNode* root) {
	Queue q;
	QueueInit(&q);

	// 不为空进队列
	if (root) {
		QueuePush(&q, root);
	}

	// 如果队列是空的,代表已经访问完所有元素
	while (!QueueEmpty(&q)) {
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%c ", front->data);

		if (front->left) {
			QueuePush(&q, front->left);
		}

		if (front->right) {
			QueuePush(&q, front->right);
		}
	}
	QueueDestroy(&q);
}

  • 分析:初始化队列,如上图,节点1入队列,进入循环,取队头节点并打印在控制台,节点1的左右节点2,4入队列,节点1出队列(队列中剩2,4);开始第二次循环,取队头节点2,2的左节点3入队列,2出队列(队列中剩4,3),以此类推,开始第三次循环后节点4的左右节点进队列,完成层序遍历。

十一、判断完全二叉树

完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。 完全二叉树就是这样一种特殊的非满二叉树:除了最后一层外其他每一层都是填满的,并且最后一层的节点都尽可能地靠左排列。如上图。


  • 具体实现步骤:判断完全二叉树不能利用递归实现,需要利用队列的相关知识(广度优先遍历)实现。
  1. 创建队列,无论树中的节点是否为空,都进入队列,根据层序遍历,将队头节点的左右子节点带入队列,在进入队列时如果遇到了第一个空节点就停止将节点带入队列,开始判断队列中的节点是否都为空。如果是,代表这棵树是完全二叉树;如果不是,代表这棵树是完全二叉树。
  • 代码:
cpp 复制代码
bool TreeComplete(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;
}

  • 队列的源码可到博主的个人码云中获取(Project_Queue)

十二、补充二叉树的性质

  1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2的i-1次方 个结点.

  2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2的h次方减1.

  3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为n2 ,则有n0 =n2 +1 ​​​​​​​

  4. 若规定根结点的层数为1,具有n个结点的满二叉树的深度h=log2(n+1) . (ps:**log2(n+1)**是log以2为底,n+1为对数)

  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:

  • 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
  • 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
  • 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

针对性质3:

cpp 复制代码
/*
* 假设二叉树有N个结点
* 从总结点数角度考虑:N = n0 + n1 + n2 ①
* 
* 从边的角度考虑,N个结点的任意二叉树,总共有N-1条边
* 因为二叉树中每个结点都有双亲,根结点没有双亲,每个节点向上与其双亲之间存在一条边
* 因此N个结点的二叉树总共有N-1条边
* 
* 因为度为0的结点没有孩子,故度为0的结点不产生边; 度为1的结点只有一个孩子,故每个度为1的结
点* * 产生一条边; 度为2的结点有2个孩子,故每个度为2的结点产生两条边,所以总边数为:
n1+2*n2 
* 故从边的角度考虑:N-1 = n1 + 2*n2 ②
* 结合① 和 ②得:n0 + n1 + n2 = n1 + 2*n2 - 1
* 即:n0 = n2 + 1
*/
相关推荐
Yan.love14 分钟前
开发场景中Java 集合的最佳选择
java·数据结构·链表
stm 学习ing17 分钟前
HDLBits训练5
c语言·fpga开发·fpga·eda·hdlbits·pld·hdl语言
冠位观测者24 分钟前
【Leetcode 每日一题】2545. 根据第 K 场考试的分数排序
数据结构·算法·leetcode
就爱学编程1 小时前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
北国无红豆2 小时前
【CAN总线】STM32的CAN外设
c语言·stm32·嵌入式硬件
单片机学习之路2 小时前
【C语言】结构
c语言·开发语言·stm32·单片机·51单片机
ALISHENGYA2 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战项目二)
数据结构·c++·算法
DARLING Zero two♡3 小时前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
graceyun4 小时前
C语言初阶习题【9】数9的个数
c语言·开发语言