数据结构:树和二叉树【超详细】

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

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

小新的主页:编程版小新-CSDN博客

1.树

1.1树的定义

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


树有一个特殊的结点,称为根结点根结点没有前驱结点。


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

因此,树是递归定义的。 

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

 

像这种子树之间有交集的就不是树。

1.2树的相关概念

结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6


叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点


非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等结点为分支结点


双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点


兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点


树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6


结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;


树的高度或深度:树中结点的最大层次; 如上图:树的高度为4


堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点


结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先


子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙


森林:由m(m>0)棵互不相交的树的集合称为森林; 

 

1.3树的表示方法 

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。

1.双亲表示法

这棵树是用顺序表表示的,我们即用顺序表存储各个节点的数据,并且同时存储其双亲节点的下标。注意:根节点没有双亲节点,所以特别记为-1。

#define MAXSIZE 10
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;//存放树的节点数据
	int parent;//存放父亲的下标
}LTNode;

struct TreeData
{
	LTNode node[MAXSIZE];//存放每个节点的数据
	int size;//存放节点的个数
};

2.孩子表示法

树的孩子表示法就是采用顺序表与链表结合的形式,用顺序表存储树的值与链表的头节点,而链表的头节点存储其孩子节点在顺序表中的下标,若没有则记为空(N)。

#define MAXSIZE 10
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType child;
	struct ListNode* next;
}LTNode;

typedef struct Child
{
	LTDataType data;
	LTNode* firstchild;//存放第一个孩子的头指针
}TChild;
struct TreeData
{
	TChild node[MAXSIZE];//存放每个节点的数据
	int size;//记录节点的个数
};

3.左孩子右兄弟表示法

最常用表示树的的方法就是左孩子右兄弟表示法,即定义两个指针,让左指针指向孩子节点,右指针指向兄弟节点。如果没有节点,则都指向NULL。

typedef int LTDataType;
struct ListNode
{
	LTDataType data;
	struct ListNode* left;//指向孩子节点
	struct ListNode* right;//指向兄弟节点
};

 

1.4树在实际中的运用

表示文件系统的目录树结构。

 

2.二叉树 

2.1二叉树的定义

一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根结点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:

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


注意:对于任意的二叉树都是由以下几种情况复合而成的:

 

2.2特殊的二叉树 

这是现实中经典的二叉树。

特殊二叉树的分类 :

1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^(K-1) ,则它就是满二叉树。


2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。(简单理解为就是第K层的节点可能不为满,最少为1个,最大就像满二叉树那样)

 

2.3二叉树的特点 

1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1)个结点.
2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1.
3. 对任何一棵二叉树, 如果度为0其叶结点个数为N0 , 度为2的分支结点个数为N2 ,则有 N0=N2 +1

/*
* 假设二叉树有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
*/

4. 若规定根结点的层数为1,具有n个结点的满二叉树的深度,h= log2(n+1)(ps:是log以2为底,n+1为对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:
1. 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

2.4二叉树的存储

2.4.1数组存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

2.4.2链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
 struct BinTreeNode* left; // 指向当前结点左孩子 
 struct BinTreeNode* right; // 指向当前结点右孩子
 BTDataType data; // 当前结点值域
}

 

2.5二叉树的遍历

二叉树的遍历方法有四种:前序遍历,中序遍历,后续遍历,层序遍历。

1. 前序遍历(Preorder Traversal 亦称先序遍历)

前序遍历的顺序是:先遍历根,在遍历左子树,右子树。在遍历左子树的时候又要先遍历根,再遍历左子树,右子树,直到遍历到空树。

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;


void PrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	printf("%d ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}


2. 中序遍历(Inorder Traversal)

中序遍历的顺序是,先遍历左子树,再遍历根,右子树。而遍历左子树,又要先遍历左子树,再依次遍历根节点,右子树…直至遍历到空树。

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;


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

	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}


3. 后序遍历(Postorder Traversal)

后序遍历的顺序是,先遍历左子树,再依次遍历右子树,根节点。而遍历左子树,又要先遍历左子树,再依次遍历右子树,根节点…直至遍历到空树。

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;


void BackOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	BackOrder(root->left);
	BackOrder(root->right);
	printf("%d ", root->data);
}

4.层序遍历(level order traversal)

层序遍历是一层一层的遍历树,上一层会带下一层。层序遍历依靠队列来实现。这里注意的是队列里存放的不是1,2,3这些数据,因为单靠这些数据话,是找不到树的左右孩子的,因此这里队列里存放的树节点的指针。

这里在声明队列的时候就做了更改。如果不能理解的话可以看一下之前队列的博客。

数据结构:链式队和循环队的原理,实现及实际应用-CSDN博客

//队列的声明
typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
	QDataType x;
	struct QueueNode* next;
}QueueNode;

代码实现:

void TreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
	{
		QueuePush(&q,root);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%d ",front->data);
		

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

下面这个是整体的实现和测试:

#include"Queue.h"

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

BTNode* Buynode(int x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;

	return newnode;
}

//创建一个树
BTNode* CreatTree()
{
	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 = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	node5->right = node7;

	return node1;
}

//层序遍历
//利用队列实现,上一层带下一层,直到队列为空的时候就结束了
//队列里面存的不是数字,而是指针,因为单靠一个值是找不到树的左右孩子的,所以这里队里里存的是树节点的指针
void TreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
	{
		QueuePush(&q,root);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%d ",front->data);
		

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

// 二叉树销毁
void TreeDestory(BTNode* root)
{
	if (root == NULL)
		return;

	TreeDestory(root->left);
	TreeDestory(root->right);
	free(root);
}

int main()
{
	BTNode* root = CreatTree();
	TreeLevelOrder(root);

	TreeDestory(root);
	root = NULL;
	return 0;
}

 

3.二叉树的相关功能

这里介绍的二叉树的功能不是很齐全,在后面学习C++后我会补充。

 二叉树节点个数
 二叉树叶子节点个数

二叉树的层数
 二叉树第k层节点个数
 二叉树查找值为x的节点
通过前序遍历的数组构建二叉树
二叉树销毁
判断二叉树是否是完全二叉树

 

4.功能实现

4.1二叉树节点的个数

思路一:我们可用前序遍历的方式,遍历树,定义一个size记录树的节点个数,如果不为空,size++

int size = 0;
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	else
	{
		size++;
	}

	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);

	return size;
}

这里的size要是全局变量,如果把size改成局部变量呢?

int BinaryTreeSize(BTNode* root)
{
	static int size = 0;
	if (root == NULL)
	{
		return 0;
	}
	else
	{
		size++;
	}

	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);

	return size;
}


int main()
{
	BTNode* root = CreatTree();
	int size1 = BinaryTreeSize(root);
	printf("%d ", size1);

	int size2 = BinaryTreeSize(root);
	printf("%d ", size2);
	
	return 0;
}

运行结果:

这是因为局部变量只会被初始化一次,当再次调用求节点个数的函数时,第一次计算得到的size的值不会被再次初始化为0,所以两次的结果不一样。 

思路二:我们可以用一个指针变量size。

void BinaryTreeSize(BTNode* root, int* psize)
{
	if (root == NULL)
	{
		return 0;
	}
	else
	{
		(*psize)++;
	}

	BinaryTreeSize(root->left,psize);
	BinaryTreeSize(root->right,psize);

}

思路三:我们采用递归的方式,树的节点的个数是左子树的节点个数+右子树的节点个数+1 

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

 

4.2二叉树叶子节点个数

叶子节点的特点是左右孩子都空,我们利用这一点来实现代码。

int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	if (root->left == NULL && root->right == NULL)
		return 1;

	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

4.3二叉树的层数

// 二叉树的层数
int TreeHight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return TreeHight(root->left) > TreeHight(root->right)
		? TreeHight(root->left) + 1 : TreeHight(root->right)+1;
}

向上面那种写法是可以的,但是效率比较低,已经比较出左子树和右子树的那个大,在后面的时候,还要在调用,然后再+1。我们可以创建两个变量记住左右树的高度。

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

4.4二叉树第k层节点个数

// 二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;

	if (k == 1)//k==1代表只有一个根节点返回1
		return 1;

	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right,k-1);
}

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

依旧采用递归的思想,先找根,根不是就往其对应的左子树,右子树中去找。

// 二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

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

	BTNode* ret1 = BinaryTreeFind(root->left, x);
	if (ret1)//如果ret1不为空,说明在左子树找到了
		return ret1;

	BTNode* ret2 = BinaryTreeFind(root->right, x);
	if (ret2)//如果ret2不为空,说明在右子树找到了
		return ret2;

	return NULL;

}

4.6通过前序遍历的数组构建二叉树

假设我们构建一个这样的树"ABD##E#H##CF##G##"

// 通过前序遍历的数组构建"ABD##E#H##CF##G##"二叉树

BTNode* BinaryTreeCreate(char* a, int* pi)
{
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}

	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	root->data = a[(*pi)++];
	root->left = BinaryTreeCreate(a, pi);
	root->right = BinaryTreeCreate(a, pi);

	return root;
}

4.7二叉树的销毁

二叉树的销毁可以利用后序遍历,使用前序遍历也可以但是在释放掉根节点之前,要把根节点存起来,使其能找到左右子树,利用后序遍历的话就规避掉了这个问题。

// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
		return;

	BinaryTreeDestory(root->left);
	BinaryTreeDestory(root->right);
	free(root);
}

4.8判断二叉树是否是完全二叉树

判断二叉树是否是完全二叉树,需要用到层序遍历的思想。

用层序遍历的思想,空也入队列。当遇到第一个空的时候,开始检查队列。若队列里都为空,则说明是完全二叉树,否则不是完全二叉树。

//判断二叉树是否是完全二叉树
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;
}
  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
哈夫曼是一种特殊的二叉树结构,用于编码和解码数据。在哈夫曼中,每个叶子节点都代表一个字符或符号,并且具有一个与之关联的权值,代表该字符或符号出现的频率或概率。根据哈夫曼的概念,我们可以通过给定的叶子节点的权值来构建哈夫曼。 对于给定的叶子节点的权值,构建哈夫曼的步骤如下: 1. 首先,根据叶子节点的权值从小到大进行排序。 2. 选取权值最小的两个叶子节点,并将它们作为两个子节点创建一个新的父节点。新父节点的权值等于这两个子节点的权值之和。 3. 将这个新的父节点插入到叶子节点中,同时删除原来的两个子节点。 4. 重复步骤2和步骤3,直到只剩下一个节点,即根节点,这个节点就是哈夫曼的根节点。 根据题目提供的例子,我们可以看到一种不是建的方法,只使用数组来模拟哈夫曼的构造过程。这种方法是通过数组来存储节点的信息,并通过一些特定的计算方式来模拟构建哈夫曼的过程。 根据题目的描述,我们需要根据叶子节点的个数和权值来生成哈夫曼,并计算所有节点的值与权值的乘积之和。这个问题可以通过构建哈夫曼的步骤来解决。首先,我们需要将叶子节点根据权值进行排序。然后,按照步骤2和步骤3构建哈夫曼,直到只剩下一个节点。最后,计算所有节点的值与权值的乘积之和。 综上所述,数据结构哈夫曼的例题是通过给定叶子节点的权值来构建哈夫曼,并计算所有节点的值与权值的乘积之和。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [好理解的哈夫曼(最优二叉树)与例题](https://blog.csdn.net/weixin_45720782/article/details/109316157)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值