实用指南:[数据结构]--二叉树详解(结尾附原码)

什么是树

树是⼀种⾮线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做 树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。

注意,树形结构间,子树之间不能有交集,否则就不是树形结构

非树形结构

结论:

⼦树是不相交的
除了根结点外,每个结点有且仅有⼀个⽗结点
⼀棵N个结点的树有N-1条边

树的相关结论

⽗结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点;如上图:A是B的⽗结点
⼦结点/孩⼦结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点; 如上图:B是A的孩⼦结点
结点的度:⼀个结点有⼏个孩⼦,他的度就是多少;⽐如A的度为6,F的度为2,K的度为0
树的度:⼀棵树中,最⼤的结点的度称为树的度; 如上图:树的度为 6
叶⼦结点/终端结点:度为 0 的结点称为叶结点; 如上图: BCHI... 等结点为叶结点
分⽀结点/⾮终端结点:度不为 0 的结点; 如上图: DEFG... 等结点为分⽀结点
兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟); 如上图: BC 是兄弟结点
结点的层次:从根开始定义起,根为第 1 层,根的⼦结点为第 2 层,以此类推;
树的⾼度或深度:树中结点的最⼤层次; 如上图:树的⾼度为 4
结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: A 是所有结点的祖先
路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列;⽐如A到Q的路径为:
A-E-J-Q;H到Q的路径H-D-A-E-J-Q
⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙
森林:由 m(m>0) 棵互不相交的树的集合称为森林;

二叉树

概念与结构

在树形结构中,我们最常⽤的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结点
加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空。

其中只存在左子树或只存在右子树,或左右子树都存在都是可以的

从上图中,我们可以得出结论

1. ⼆叉树不存在度⼤于 2 的结点
2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树

现实中的二叉树

特殊的二叉树

满二叉树

⼀个⼆叉树,如果每⼀个层的结点数都达到最⼤值,则这个⼆叉树就是满⼆叉树。也就是说,如果⼀ 个⼆叉树的层数为 K ,且结点总数是 2k − 1 ,则它就是满⼆叉树。

完全二叉树

完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为 K 的,有 n 个 结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从 1 n 的结点⼀⼀对应时称 之为完全⼆叉树。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。

注意,假设二叉树的高度为h层,那么前h-1层的二叉树是满的,最后第h层,可满可不满,但节点都必须从左往右的

二叉树重要结论

根据满⼆叉树的特点可知:
1)若规定根结点的层数为 1 ,则⼀棵⾮空⼆叉树的第i层上最多有 2i−1 个结点
2)若规定根结点的层数为 1 ,则深度为 h 的⼆叉树的最⼤结点数是 2h − 13)若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度 h = log2 (n + 1) ( log
以2为底, n+1 为对数)

二叉树很像我们高中学的,一个首元素为1,公比为2的等比数列,

上面结论也确实可以用等比数列的公式进行推导

二叉树的存储结构

⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。

顺序结构

顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。
现实中我们通常把堆(⼀种⼆叉树)(下面会讲)使⽤顺序结构的数组来存储,需要注意的是这⾥的堆和操作系统 虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段

链式结构

⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法 是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩 ⼦所在的链结点的存储地址
分为二叉链和三叉链,此处讲解二叉链

堆(实现顺序结构的二叉树)

此处若想了解,可以翻看我上一篇文章,专门进行了讲解

实现链式二叉树

创建二叉树基本结构

⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组 成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 , 其结构如下:
typedef struct BinaryTree
{
	struct BinaryTree* left;//指向左节点的孩子
	struct BinaryTree* right;//指向右节点的孩子
	int val;
}BTree;

导入二叉树过于麻烦,这里我们手动创建一颗二叉树

BTree* ByNode(int x)
{
	BTree* newnode = (BTree *) malloc(sizeof(BTree));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(1);
	}
	newnode->val = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}
BTree* node1 = ByNode(1);
BTree* node2 = ByNode(2);
BTree* node3 = ByNode(3);
BTree* node4 = ByNode(4);
BTree* node5 = ByNode(5);
BTree* node6 = ByNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;

前序中序后序遍历

即访问顺序不同
前序遍历 :
根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面 (先访问根)
中序遍历 :
根在中,左子树在前,从左往右,一棵树的左子树永远在根前面,根又永远在右子树前面(第二访问根)
后序遍历:
根在后,从左往右,一棵树的左子树永远在右子树前面,右子树永远在根(最后访问根)
以下代码都是基于该二叉树 (不是会进行说明)
前序遍历

代码实现(使用递归)

void frontroot(BTree* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ",root->val);
	frontroot(root->left);//最大的左子树全部访问完,递归才会回到第一次由main函数进入到此函数的位置,然后去访问右子树
	frontroot(root->right);
}

流程图详解

函数递归栈帧图

如果还不是足够明白,可以去看看函数栈帧相关的知识

遍历结果 1  2   3   N   N  N   4   5  N N 6 N N    (N为NULL)

中序遍历

与前序一样,只是顺序不同

void midroot(BTree* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	midroot(root->left);//最大的左子树全部访问完,递归才会回到第一次由main函数进入到此函数的位置,然后去访问根
	printf("%d ",root->val);
	midroot(root->right);
}

遍历结果:  N  3  N  2  N  1    N  5  N  4    N   6   N

后序遍历

与前面类似

void lastroot(BTree* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	lastroot(root->left);//最大的左子树全部访问完,递归才会回到第一次由main函数进入到此函数的位置,然后去访问右子树
	lastroot(root->right);
	printf("%d ", root->val);
}

遍历结果: N   N   3   N   2   N   N  5    N   N  6   4  1

二叉树销毁

void BinaryTreeDestory(BTree** root)//改变指针的指向,传二级指针
{
	//使用后序遍历
	if (*root == NULL)
		return;
	BinaryTreeDestory((*root)->left));
	BinaryTreeDestory((*root)->right));
	*root = NULL;
}

层序遍历

除了先序遍历、中序遍历、后序遍历外,还可以对⼆叉树进⾏层序遍历。设⼆叉树的根结点所在层数 为1,层序遍历就是从所在⼆叉树的根结点出发,⾸先访问第⼀层的树根结点,然后从左到右访问第2 层上的结点,接着是第三层的结点,以此类推,⾃上⽽下,⾃左⾄右逐层访问树的结点的过程就是层序遍历
实现需要借助其他数据结构:队列(队列详细实现在前面已讲,此处直接使用
void LevelOrder(BTree* root)
{
    QE qe;
	QEInit(&qe);
    if(root)
	QEPush(&qe,root);//传的是指针,要将QEDateType这个重命名改一下
	while(QEEmptert(&qe))
	{
		BTree* froot = QEFront(&qe);
		QEPop(&qe);
		if (froot->left);
			QPush(&qe, root->left);
		if(froot->right)
			QPush(&qe,root->right)
	}
}
//队列先进先出,先进一个,再出一个,然后将出的这个头节点的孩子插入到队列中

其他相关操作

节点的个数

即访问每个节点再统计,此处采用前序遍历访问

int NodeNum2(BTree* root)
{
	//空返回0
	//不为空,返回左子树加右子树再加一
	if (root == NULL)
		return 0;
	return NodeNum2(root->left) + NodeNum2(root->right) + 1;
}

叶子个数

即该节点没有一个孩子的节点个数

int Treeleaf(BTree* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	return Treeleaf(root->left) + Treeleaf(root->right);
}

二叉树第k层节点个数

假设从上往下数有h层,第k层在h层内,那么每次调用k-1,当k==,即是第k层

int BTreeKSize(BTree*root,int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
	{
		return 1;
	}
	return (BTreeKSize(root->left, k - 1) + BTreeKSize(root->right, k - 1));
}

二叉树的高度

可以先统计左子树高度,再统计右子树高度,比较哪个大取哪个,最后加上根节点1

int TreeHigh(BTree* root)
{
	//数据不多的情况下可以,多的话,左子树与右子树的个数没有存着,会多次调用,叶子最明显
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	/*return (TreeHigh(root->left) > TreeHigh(root->right) ? TreeHigh(root->left) + 1 : TreeHigh(root->right) + 1);*/
	//解决方法一:将左子树与右子树存起来TreeHigh(root->left) > TreeHigh(root->right)
	int left = TreeHigh(root->left);
	int right = TreeHigh(root->right);
	//如果left == right,则随机返回一个
	//return (left > right ? left + 1 : right + 1);
	//解决方法二;使用fmax函数
	return fmax(TreeHigh(root->left), TreeHigh(root->right)) + 1;//要使用头文件:math.h
}

⼆叉树查找值为x的结点

BTree* TreeXNode(BTree*root,int x)
{
	//前序遍历
	if (root == NULL)
		return NULL;
	if (root->val == x)
		return root;
	BTree * rootl =  TreeXNode(root->left, x);
	if (rootl)
		return rootl;
	BTree* rootr = TreeXNode(root->right, x);
	if (rootr)
		return rootr;
	 //可改写为
	//return (TreeXNode(root->right, x));
		return NULL;
}

判断是不是完全二叉树

使用的方法与前面层序遍历类似

void LevelOrder(BTree* root)
{
    QE qe;
	QEInit(&qe);
    if(root)
	QEPush(&qe,root);//传的是指针,要将QEDateType这个重命名改一下
	while(QEEmptery(&qe)
	{
		BTree* froot = QEFront(&qe);
		QEPop(&qe);
        if(front == NULL)//如果出的数据是空,那么就可以开始判断了
            break;
		if (froot->left);
			QPush(&qe, root->left);
		if(froot->right)
			QPush(&qe,root->right)
	}
    while(QEEmptery(&qe)
    {
        BTree*front = QEFront(&qe);
        if(froot == NULL)
        return false;
        QEPop(&qe);
    }
    return true;
}

posted @ 2025-11-29 08:02  yangykaifa  阅读(0)  评论(0)    收藏  举报