数据结构-树

在这里插入图片描述

1. 树的定义及结构

1.1 定义

​ 树是一种非线性的数据结构,其是由n(n>=0)个有限节点组成的一个具有层次关系的集合。

​ 注:树有一个特殊的结点,称为根节点,根节点没有前驱结点;
​ 除根节点外,其余节点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继;

​ 因此,树是递归定义的。

​ 图示如下:

在这里插入图片描述

注意:1. 树的子树是不能相交的

​ 2. 除了根节点外,每个结点有且仅有一个父节点;

​ 3. 一颗N个节点的树有N-1条边

1.2 树的相关概念

(注:下列的“如上图”皆指1.1中的图示)

节点的度:一个节点含有的子树的个数称为该节点的度;如上图A结点有B、C、D三个子树,即A的度为3。

叶节点或终端节点:度为0的节点称为叶节点;如上图:K、L、M、N、G等结点为叶节点。
非终端节点或分支节点:度不为0的节点;如上图:B、C、E、F、I等结点皆为分支节点。
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图:A为B、C、D的父节点。

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;如上图:B、C、D为A的子节点。
兄弟节点:具有相同父节点的节点互称为兄弟节点;如上图:B、C、D为兄弟节点。
树的度:一棵树中,最大的节点的度称为树的度;如上图:A的度最大,即树的度为3。
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4。
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图,K、M为堂兄弟节点。
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是其它所有节点的祖先。
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有其它节点都是A的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林;

1.3 树的性质

  1. 树的节点数=树的总边数+1

    理解:有多少条边,就有多少个子节点,再加上根节点,就得到了树的节点数。

  2. 度为n的树中,第i层最多有节点数为
    n i − 1 n^{i-1} ni1

​ 理解:因为时求最多的节点个数,所以假设每个节点的度都为n,那么第i层上的节点数 = n×第i-1层节点的个数 = n×n×第i-2层节点的个数= … = n×n×…(共i-1个n)×第一层的节点个数 = n的i-1次方×1 = n的i-1次方。

  1. 高度为h、度为n的树最多能有有节点数量为
    ( n h − 1 ) / ( n − 1 ) (n^{h}-1)/(n-1) (nh1)/(n1)

​ 理解:根据性质2我们可以求得每一层最多有多少个节点,然后将每一层的节点个数相加,通过等比相加运算即可得到结果。

  1. 具有m个节点、度为n的树最小高度为
    l o g n ( m × ( n − 1 ) + 1 ) log_n(m×(n-1)+1) logn(m×(n1)+1)
    ​ 理解:要求最小高度,那么每一层的节点数应该最多,我们假设其高度为h,根据性质3那么有

m < = ( n h − 1 ) / ( n − 1 ) m<=(n^{h}-1)/(n-1) m<=(nh1)/(n1)

​ 通过上式即可求得h。

2. 二叉树

2.1 定义

​ 二叉树是每个节点的度(而非树的度)最多为2的树结构。它由以下5种基本形态复合而成:

在这里插入图片描述

2.2 性质

  1. 二叉树第i层的节点数最多为
    2 i − 1 ( i > = 1 ) 2^{i-1}(i>=1) 2i1(i>=1)
    ​ 理解:要使得第i层的节点数最多,那么每一层每一个节点的度都为2,则第i层的节点数 = 2×第i-1层节点数 = 2×2×第i-2层节点数 = … = 2×2×…(共i-1个2相乘)×1(第一层节点数)= 2的i-1次方

  2. 深度为k的二叉树最多能拥有的节点数为
    2 k − 1 2^k-1 2k1
    ​ 理解:由性质1我们可得每一层最多节点数,然后把它们通过等比数列相加运算即可得到。

  3. 包含n个节点的二叉树的高度至少为
    l o g 2 ( n + 1 ) log_2(n+1) log2(n+1)
    ​ 理解:假设高度为h,那么由性质2可得:
    2 h − 1 > = n 2^h-1>=n 2h1>=n
    ​ 解h即可得结果。

  4. 在任意一颗二叉树中,令度为0的节点个数为n0,度为2的节点个数为n2,那么有
    n 0 = n 2 + 1 n0 = n2+1 n0=n2+1
    ​ 理解:假设度为1的节点个数为n1,那么节点总个数
    n = n 0 + n 1 + n 2 n = n0+n1+n2 n=n0+n1+n2
    ​ 对于一棵树,树的节点数等于总边数+1,所以有
    n = n 0 × 0 + n 1 × 1 + n 2 × 2 + 1 n = n0×0+n1×1+n2×2+1 n=n0×0+n1×1+n2×2+1
    ​ 二者联立即可得到
    n 0 = n 2 + 1 n0 = n2+1 n0=n2+1

2.3 特殊的二叉树

2.3.1 满二叉树

​ 定义:每一层节点数都达到满足二叉树要求的最大值,即:若假设某二叉树的层数为k,根据2.2性质3,若它的节点总数等于2^k-1,那么该树为满二叉树。如图所示:

图中二叉树的层数为3,其节点总数=2^3-1 = 7,所以它是满二叉树。

2.3.2 完全二叉树

​ 定义:若二叉树深度为k,除了第k层外,其它各层节点数都达到最大个数且第k层所有节点都连续从左向右依次排列,则该二叉树为完全二叉树。完全二叉树是由满二叉树引出来的,对于深度为K、节点数为n的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。图示如下:

在这里插入图片描述

2.4 存储方式

2.4.1 顺序结构

​ 顺序结构即用数组形式来存储,但非完全二叉树会造成空间的浪费,实际使用时一般只有才会使用数组存储。后续将会单独开一章讲述相关内容,此处不再讲述。

2.4.2 链式结构

​ 链式结构即用链表来表示一棵二叉树,通过用链来表示元素的逻辑关系,使用数据域和左右指针,通过用左右指针分别指向左右孩子所在的地址将节点连接起来。示意图如下:
在这里插入图片描述

结构代码定义如下:

typedef int BTdata;//数据类型
typedef struct BinaryTreeNode
{
	struct BinaryTreeBode* left;//左孩子
	struct BinaryTreeBode* right;//右孩子
	BTdata val;//数据
}BTNode;

2.5 二叉树部分功能实现

2.5.0 前言

为了方便理解,我们在实现功能之前先手动创建一个二叉树:

//创建节点
BTNode* BuyNode(BTdata x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->val = x;
	node->left = NULL;
	node->right = NULL;
	return node;
}
int main()
{
	//手动构建二叉树
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);
	BTNode* node7 = BuyNode(7);
	//手动连接节点
	node1->left = node2;
	node1->right = node3;
	node2->left = node4;
	node2->right = node5;
	node3->left = node6;
	node3->right = node7;
	return 0;
}

创建的二叉树如下:

在这里插入图片描述

后面的功能均在此二叉树基础上实现。

2.5.1 四种遍历方式

1. 前序遍历

​ 定义:访问根节点的操作在遍历其每棵左右子树之前,即:对于每个根节点及其左右子树,先访问根节点,再访问其左子树,最后访问右子树。

为方便理解这里用上面创建的二叉树大致展开讲讲,如图所示:
在这里插入图片描述

最终遍历结果为:1 2 4 NULL NULL 5 NULL NULL 3 6 NULL NULL 7 NULL NULL(根据情况看是否需要输出空节点)

我们将遍历结果简单处理如下:

在这里插入图片描述

​ 我们可以发现,每个小板块都是遍历根节点->左子树->右子树的过程,即二叉树的遍历是很多个相同的过程的集合,因此我们在代码实现时,需要用到递归思想。

代码实现如下:

void PrevOrder(BTNode* root)
{
	if (root == NULL)//根节点为空
	{
		printf("NULL ");//根据需要看是否需要打印空节点,不需要可删去
		return;//返回上一层
	}
	//根节点不为空
	printf("%d ", root->val);//访问根节点
	PrevOrder(root->left);//访问左子树
	PrevOrder(root->right);//访问右子树
}

​ 不太理解代码的小伙伴可以自行画一画递归展开图。

在主函数中调用该函数可以得到如下结果:

在这里插入图片描述

2. 中序遍历

​ 定义:访问根节点的操作在遍历其每棵左右子树之间,即:对于每个根节点及其左右子树,先访问其左子树,再访问根节点,最后访问右子树。这里不再展开说明。

最终遍历结果为:NULL 4 NULL 2 NULL 5 NULL 1 NULL 6 NULL 3 NULL 7 NULL

我们将遍历结果简单处理如下:

在这里插入图片描述

我们可以发现,每个小板块都是遍历左子树->根节点->右子树的过程。

代码实现如下:

void InOrder(BTNode* root)
{
	if (root == NULL)//根节点为空
	{
		printf("NULL ");
		return;//返回上一层
	}
	//根节点不为空
	InOrder(root->left);//访问左子树
	printf("%d ", root->val); //访问根节点
	InOrder(root->right);//访问右子树
}

在主函数中调用该函数可以得到如下结果:

在这里插入图片描述

3. 后序遍历

​ 定义:访问根节点的操作在遍历其每棵左右子树之后,即:对于每个根节点及其左右子树,先访问其左子树,再访问右子树,最后访问根节点。

最终遍历结果为:NULL NULL 4 NULL NULL 5 2 NULL NULL 6 NULL NULL 7 3 1

我们将遍历结果简单处理如下:

在这里插入图片描述

我们可以发现,每个小板块都是遍历左子树->右子树->根节点的过程。

代码实现如下:

void PostOrder(BTNode* root)
{
	if (root == NULL)//根节点为空
	{
		printf("NULL ");
		return;//返回上一层
	}
	PostOrder(root->left);//访问左子树
	PostOrder(root->right);//访问右子树
	printf("%d ", root->val);//访问根节点
}

在主函数中调用该函数可以得到如下结果:

在这里插入图片描述

4. 层序遍历

定义:从一棵二叉树的根节点开始,从上至下,从左至右依次遍历每一层的每一个节点就是层序遍历。即遍历结果应该为:1 2 3 4 5 6 7

​ 那么要怎样才能实现层序遍历呢?这里我们需要用到队列,利用其先进先出的特点,关于队列可以看一看我的这篇文章:数据结构-队列

​ 这里我直接使用文章中我已经写好的队列,由于我们需要装进队列的是节点,因此对于队列装的数据类型的定义需要做出以下更改:

typedef struct BTNode* QDataType;//这里BTNode为2.4.2中定义的二叉树节点结构

​ 遍历过程:首先判断树是否为空树,若不是则将根节点放入队列,以队列是否为空作为循环条件,在非空时循环做以下行动:

  1. 取出队列中头节点的元素

  2. 打印该节点内的数据

  3. 判断其左右孩子节点是否为空,不为空则将孩子节点放入队列中

  4. 头删去队列中的头节点

过程图示如下:

在这里插入图片描述

代码实现如下:

void LevelOrder(BTNode* root)
{
	Que q;//创建队列
	QueueInit(&q);//初始化队列
	if (root)//判断根节点是否为空
		QueuePush(&q, root);//不为空根节点入队
	while(!QueueEmpty(&q))//当队列不为空时
	{
		BTNode* front = QueueFront(&q);//获取头节点
		printf("%d ", front->val);//打印该节点内的数据
		if (front->left)//左孩子不为空
			QueuePush(&q, front->left);//入队

		if (front->right)//右孩子不为空
			QueuePush(&q, front->right);//入队

		QueuePop(&q);//头删队列头节点
	}
	printf("\n");
}

在主函数中调用该函数可以得到如下结果:

在这里插入图片描述

2.5.2 二叉树结点个数

思路:

1.对于一棵树,首先判断其根节点是否为空,为空则该树总结点个数为0;

​ 2.若不为空,则总结点个数等于左子树结点个数+右子树结点个数+1(根节点);

​ 3.对于每个左右子树,都有1、2.

不难看出我们在此同样可以使用递归思想。

代码如下:

int TreeSize(BTNode* root)
{
	if (root == NULL)//根节点为空则返回0
		return 0;
	return TreeSize(root->left) + TreeSize(root->right) + 1;//否则返回左子树结点个数+右子树结点个数+1
}

我们可以简化成:

int TreeSize(BTNode* root)
{
	return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

2.5.3 二叉树叶子结点的个数

思路:

1.对于一棵树,首先判断其根节点是否为空,为空则该树叶子结点个数为0;

​ 2.若不为空,判断其左右孩子是否为空,若都为空,则该树的叶子结点个数为1;

​ 3.若其孩子结点不都为空,则叶子结点个数等于左子树叶子结点个数+右子树叶子结点个数;

​ 4.对于每个左右子树,都有1、2、3.

同理,我们需要用到递归思想。

代码如下:

int TreeLeafSize(BTNode* root)
{
	if (root == NULL)//若根结点为空
		return 0;//返回0
	if (root->left == NULL && root->right == NULL)//若左右孩子都为空,说明是叶子结点
	{
		return 1;//返回1
	}
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);//孩子结点不都为空,则叶子结点个数等于左子树叶子结点个数+右子树叶子结点个数
}

2.5.4 第k层结点个数

这里我们将起始层记为第1层。

思路:

1.如果根节点为空,则结点数为0;

​ 2.如果根节点不为空,当k=1时,那么只有1个结点;

​ 3.当k>1时,第k层的结点个数=左子树中第k-1层的结点个数+右子树中第k-1层的结点个数;

​ 4.对于每棵左右子树都有1、2、3.

代码如下:

int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);//防止k不满足条件
	if (root == NULL)//如果根节点为空
		return 0;//结点数为0
	if (k == 1)//根结点不为空,当k=1时
		return 1;//结点数为1
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);//当k>1时,第k层的结点个数=左子树中第k-1层的结点个数+右子树中第k-1层的结点个数
}

为便于理解,我们通过下面的图做一个讲解,假设我们找第3层结点的个数。

在这里插入图片描述

图示如下:在这里插入图片描述

递归展开图如下:

在这里插入图片描述

2.5.5 查找值为x的结点

思路:通过遍历的方式进行查找,这里我们使用前序查找,代码如下:

BTNode* TreeFind(BTNode* root, BTdata x)
{
	assert(root);
	if (root == NULL)//如果根节点为空
		return NULL;//返回NULL表示没找到
	if (root->val == x)//不为空,对比根节点的值是否与要查找额值相等
		return root;//如果相等返回该结点
	BTNode*pos =  TreeFind(root->left,x);//在左子树中寻找,并将结果储存在pos中
	if (pos != NULL)//判断pos是否为空
		return pos;//不为空说明找到了,返回pos结点
	return TreeFind(root->right, x);//左子树中没找到,在右子树中找,返回结果
}

2.5.6 计算二叉树的高度

思路:

1.首先判断根节点是否为空,为空则高度为0;

​ 2.若不为空,分别计算左右子树的高度;

​ 3.若左子树的高度大于右子树的高度,则二叉树的高度=左子树高度+1,否则等于右子树高度+1;

(有小伙伴可能会想为什么再不考虑左右子树高度相等的情况,因为如果两者相等,那么无论返回谁的高度+1结果都一样)

​ 4.对于每课左右子树都有1、2、3.

代码如下:

int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

不太明白的小伙伴可以像我在2.5.5中一样根据一棵树画一画递归展开图。

有的小伙伴可能会用这样的代码:

int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	return TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) + 1 : TreeHeight(root->right);
}

但这样并不好,比较好左右子树哪边更高后会再次计算更高那边的高度,效率相比第一种较低。

2.5.7 判断一棵树是否是完全二叉树

思路:

1.判断根节点是否为空,为空则为完全二叉树;

​ 2.若不为空,将根节点入队;

​ 3.在队列不为空的条件下,通过层序遍历的方式依次出队、入队、头删(包括空结点),当出队元素为空 时,提前结束遍历;

​ 4.若队列不为空,再次循环进行出队、头删,如果直到头删到队列为空之前有非空结点出队,则说明该树不 是完全二叉树,循环结束若不再有非空结点出队,则说明是完全二叉树.

代码如下:

int TreeComplete(BTNode* root)
{
	Que q;//创建队列
	QueueInit(&q);//初始化队列
	if (root)//若根节点不为空
		QueuePush(&q, root);//将根节点入队
	while (!QueueEmpty(&q))//在队列不为空时进行层序遍历
	{
		BTNode* front = QueueFront(&q);
		if (front == NULL)//若出队元素为空
		{
			break;//提前结束遍历
		}
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
		QueuePop(&q);
	}
	//若已经遇到空结点,如果队列中的后面结点还有非空则该树不是完全二叉树
	while (!QueueEmpty(&q))//若队列中还有元素
	{
		BTNode* front = QueueFront(&q);//出队
		QueuePop(&q);//头删
		if (front != NULL)//若还有非空元素
		{
			QueueDestroy(&q);//销毁队列
			return false;//则不是完全二叉树
		}
	}
    //队列中不再有非空结点则说明是完全二叉树
	QueueDestroy(&q);//销毁队列
	return true;
}

本次内容就到这里啦,有问题欢迎各位小伙伴提出指正!
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值