二叉树与堆:从概念到实战全解析

「C++ 40 周年」主题征文大赛(有机会与C++之父现场交流!) 10w+人浏览 572人参与

目录

一、树

1.树的概念与结构:

2.树的性质:

3.树相关术语

4.树的表示

5.树形结构的实际应用场景

二、二叉树

1.概念与结构

2.二叉树的性质

3.特殊的二叉树

1)满二叉树

2)完全二叉树

三、二叉树存储结构

1.顺序结构

2.链式结构

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

1.堆的概念与结构

2.堆的性质

3.堆的实现

1)创建堆结构

2)初始化堆、销毁堆、打印堆

3)向上调整算法

代码实现:

向上调整算法建堆的时间复杂度:

4)向下调整算法

代码实现:

向下调整算法建堆的时间复杂度:

5)入堆

6)出堆

7)取堆顶

五、堆的应用(算法题)

1.堆排序

版本一:借助堆的结构

版本二:借助堆的思想

2.TOP-K问题

六、博主手记(附解题思路)

前言:

本文将会给大家介绍树和二叉树,以及二叉树中的存储结构,还有顺序结构的二叉树的代码实现,最后还有相应的堆的实现的题目,那么让我们来一起探索吧!

一、树

1.树的概念与结构:

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

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

除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1T2……Tm ,其中每⼀个集合

Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。因此,树是递归定义的。

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

2.树的性质:

子树是不相交的(如果存在相交就是图了,图以后得课程会有讲解)
除了根结点外,每个结点有且仅有⼀个父结点
⼀棵N个结点的树有N-1条边

3.树相关术语

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

4.树的表示

struct TreeNode
{
    struct Node* child; // 左边开始的第⼀个孩⼦结点
    struct Node* brother; // 指向其右边的下⼀个兄弟结点
    int data; // 结点中的数据域
};

5.树形结构的实际应用场景

文件系统是计算机存储和管理文件的⼀种方式,它利⽤树形结构来组织和管理文件和文件夹。在文件系统中,树结构被⼴泛应用,它通过⽗结点和⼦结点之间的关系来表示不同层级的文件和文件夹之间的关联。

二、二叉树

1.概念与结构

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

2.二叉树的性质

1.二叉树不存在度大于 2 的结点

2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

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

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

3.特殊的二叉树

1)满二叉树

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

2)完全二叉树

完全二叉树是由满二叉树而引出来的。对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每⼀个结点都与深度为K的满二叉树中编号从 1 ⾄ n 的结点⼀⼀对应时称之为完全二叉树。
要注意的是满二叉树是⼀种特殊的完全二叉树。

三、二叉树存储结构

1.顺序结构

顺序结构存储就是使用数组来存储,⼀般使用数组只适合表示完全⼆叉树,因为不是完全⼆叉树会有空间的浪费,完全⼆叉树更适合使用顺序结构存储。

2.链式结构

⼆叉树的链式存储结构是指,用 链表 来表示⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 。链式结构⼜分为⼆叉链和三叉链。 当前我们学习中⼀般都是⼆叉链。后 ⾯课程学到⾼阶数据结构如红⿊树等会⽤到三叉链。

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

1.堆的概念与结构

它的所有元素按完全⼆叉树的顺序存储方式存储,在⼀个⼀维数组中,则称为堆。(堆就是一种完全二叉树)。
堆还分为大堆(大根堆)和小堆(小根堆)。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

2.堆的性质

1)堆的某个节点的值总是不大于或不小于其父节点。

2)堆总是一颗完全二叉树。

二叉树性质
对于具有 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 否则⽆右孩⼦

3.堆的实现

1)创建堆结构

//定义堆的结构
typedef int HPDataType;
typedef struct Heap {
	HPDataType* arr;
	int size;
	int capacity;
}HP;

2)初始化堆、销毁堆、打印堆

因为这部分与顺序表的结构完全一样,所以给大家合并到一起了

//初始化堆
void HPInit(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}
//销毁堆
void HPDestory(HP* php)
{
	assert(php);
	if (php->arr != NULL)
	{
		free(php->arr);
	}
	php->arr = NULL;
	php->size = php->capacity = 0;
}
//打印堆
void HPPrint(HP* php)
{
	assert(php);
	for (int i = 0;i < php->size;i++)
	{
		printf("%d ", php->arr[i]);
	}
	printf("\n");
}

3)向上调整算法

向上调整算法在向堆中插入数据的时候使用:
先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后。
插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双双亲往上调整到合适位置即可。

代码实现:
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//建大堆:>
		//建小堆:<
		if (arr[child] > arr[parent])
		{
			//交换
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}

其中的交换函数如下:

//交换
void Swap(int* x, int* y)
{
	int temp = *x;
	*x = *y;
	*y = temp;
}
向上调整算法建堆的时间复杂度:
for(int i = 0;i < n;i++)
{
    AdjustUp(arr,i);
}
向上调整算法建堆时间复杂度为: O ( n ∗ log 2 n ),证明见博主手记部分。

4)向下调整算法

向下调整算法在删除堆顶数据的时候使用:
删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进行向下调整算法。

代码实现:
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
        //先在两个孩子之间找最大的
		//建大堆:<
		//建小堆:>
		if (arr[child] < arr[child + 1] && child + 1 < n)
		{
			child++;
		}
		//孩子与父亲比较
		//建大堆:<
		//建小堆:>
		if (arr[parent] < arr[child])
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}
向下调整算法建堆的时间复杂度:
//数组建堆——向下调整算法建堆
for (int i = (n - 1 - 1) / 2;i >= 0;i--)
{
	AdjustDown(arr, i, n);
}
向下调整算法建堆时间复杂度为: O ( n),证明见博主手记部分。

5)入堆

先插入数据(与顺序表方法相同),再向上调整算法

//入堆
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//判断空间是否足够,增容
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
		if (temp == NULL)
		{
			perror("malloc");
			exit(1);
		}
		php->arr = temp;
		php->capacity = newCapacity;
	}
	php->arr[php->size] = x;
	//向上调整算法
	AdjustUp(php->arr, php->size);
	php->size++;
}

6)出堆

先判断堆是否为空,然后先将arr[0]与arr[size-1]交换位置,再将最后的arr[size-1]直接去掉,再向下调整算法。

出堆前要先定义一个判断堆是否为空的函数

//判断堆是否为空
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
//出堆
void HPPop(HP* php)
{
	assert(!HPEmpty(php));
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	//向下调整算法
	php->size--;
	AdjustDown(php->arr, 0, php->size);
}

7)取堆顶

//取堆顶
HPDataType HPTop(HP* php)
{
	assert(!HPEmpty(php));
	return php->arr[0];
}

五、堆的应用(算法题)

1.堆排序

版本一:借助堆的结构

基于已有数组建堆、取堆顶元素完成排序版本
前提:必须提供堆的结构和相应的函数
//堆排序——版本一(要借助堆的结构和基本函数)
void HeapSort1(int* arr, int n)
{
	//先建堆
	HP hp;
	HPInit(&hp);
	//将数组中的数据存放到堆中
	for (int i = 0;i < n;i++)
	{
		HPPush(&hp, arr[i]);
	}
	//运用堆排序
	int i = 0;
	while (!HPEmpty(&hp))
	{
		int top = HPTop(&hp);
		arr[i++] = top;
		HPPop(&hp);
	}
	HPDestory(&hp);
}

版本二:借助堆的思想

无需提供堆的结构和函数,直接使用数组建堆,建完堆后首尾相换,交换后将堆尾的数据删除,再从堆顶进行向下调整算法,如此循环到堆中的数据全部出堆。

//堆排序——版本二(运用堆的思想,不用堆的结构)
void HeapSort2(int* arr, int n)
{
	//将数组建堆——向下调整算法建堆
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(arr, i, n);
	}
	//数组首尾交换
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end--);
	}
}

2.TOP-K问题

TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常⼤,排序就不太可取了(可能数据都不能⼀下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1)用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

void TopK()
{
	int k = 0;
	printf("请输入K:\n");
	scanf("%d", &k);
	//读取数据
	FILE* fout = fopen("data.txt", "r");
	if (fout == NULL)
	{
		perror("fopen");
		exit(1);
	}
	//开辟k个空间
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc");
		exit(2);
	}
	//读取k个文件中的数据放入到数组中
	for (int i = 0;i < k;i++)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}
	//数组建堆-向下调整算法建堆
	//找最大的前k个数,建小堆
	for (int i = (k - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(minHeap, i, k);
	}
	//遍历剩下的n-k个数,与堆顶比较
	int data = 0;
	while (fscanf(fout, "%d", &data) != EOF)
	{
		if (minHeap[0] < data)
		{
			minHeap[0] = data;
			AdjustDown(minHeap, 0, k);
		}
	}
	//打印堆中的数据
	for (int i = 0;i < k;i++)
	{
		printf("%d ",minHeap[i]);
	}
	printf("\n");
	fclose(fout);
}

给大家一个造数据的函数,大家可以自己来试试这个TopK函数。

void CreateNDate()
{
	// 造数据
	int n = 100000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < n; ++i)
	{
		int x = (rand() + i) % 1000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

六、博主手记(附解题思路)

结语:

本次的二叉树-堆的知识分享就结束了,下一篇我会分享二叉树链式结构,感谢喜欢,欢迎关注,喜欢的可以三连博主,3Q!

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值