07.【数据结构-C语言】二叉树-堆-顺序结构(树、二叉树、堆的概念,堆实现,向上调整&向下调整,建堆时间复杂度,堆的应用:堆排序、TOP-K问题)

目录

1. 树的概念及结构

1.1 树的概念

1.2 树的相关概念

1.3 树的表示

1.4 树在实际中的运用(表示文件系统的目录树结构)

2. 二叉树的概念及结构

2.1 概念

2.2 现实中的二叉树

2.3 特殊的二叉树(满二叉树、完全二叉树)

2.4 二叉树的性质

2.5 二叉树的存储结构

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

3.1 二叉树的顺序结构

3.2 堆的概念及结构

3.3 堆的实现(完整版代码)

3.3.1 堆结构体声明(同顺序表)

3.3.2 初始化&销毁(同顺序表)

3.3.3 向上调整

3.3.4 插入(同顺序表尾插)

3.3.5 向下调整

3.3.6 删除

3.3.7 取堆顶&判空

3.4 数组直接调整成堆(向下调整)

3.5 建堆时间复杂度(详细证明)

4. 堆的应用

4.1 堆排序

4.2 TOP-K问题


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 树的表示

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

typedef int DataType;
struct Node
{
	struct Node* firstChild1; // 第一个孩子结点
	struct Node* pNextBrother; // 指向其下一个兄弟结点
	DataType data; // 结点中的数据域
};

1.4 树在实际中的运用(表示文件系统的目录树结构)

2. 二叉树的概念及结构

2.1 概念

        一棵二叉树是结点的一个有限集合,该集合:

                1. 或者为空

                2. 由一个根结点加上两棵别称为左子树和右子树的二叉树组成

        从上图可以看出二叉树的特点

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

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

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

2.2 现实中的二叉树

2.3 特殊的二叉树(满二叉树、完全二叉树)

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

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

2.4 二叉树的性质

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

        2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2^{h}-1.(等比数列求和公式)

        3. 对任何一棵二叉树, 如果度为0其叶结点个数为n_{0},度为2的分支结点个数为n_{2} ,则有n_{0}=n_{2}+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=log_{2}(n+1). (ps:是log以2为底,n+1为对数)

证明:

        等比数列a_{1}=2^{0},a_{2}=2^{1},a_{3}=2^{2},......a_{n}代表层数,2为公比)

        等比数列求和公式:S_{n}=a_{1}\times \frac{1-q^{n}}{1-q}

        代入公式:n=S_{n},h=nn=S_{n}代表总节点数,h=n代表层数)

        得:n=a_{1}\times \frac{1-q^{h}}{1-q}

        又:a_{1}=1,q=2

        代入得:n=2^{h}-1

        得:h=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.5 二叉树的存储结构

        二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构

        1. 顺序存储

        顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

        2. 链式存储

        二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。

typedef int BTDataType;

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

// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* parent; // 指向当前结点的双亲
struct BinTreeNode* left; // 指向当前结点左孩子
struct BinTreeNode* right; // 指向当前结点右孩子
BTDataType data; // 当前结点值域
};

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

3.1 二叉树的顺序结构

        普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

3.2 堆的概念及结构

        如果有一个关键码的集合K=\left \{ k_{0},k_{1},k_{2},...,k_{(n-1)} \right \},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:k_{i}<=k_{2i+1}k_{i}<=k_{2i+2} (或k_{i}>=k_{2i+1}k_{i}>=k_{2i+2}) i = 0,1,2,…,  则称为小堆(或大堆)。(即:小堆:所有根都比其子节点小大堆:所有根都比其子节点大)。将根结点最大的堆叫做最大堆(大根堆,大堆),根结点最小的堆叫做最小堆(小根堆、小堆)。

        堆的性质:

                1. 堆中某个结点的值总是不大于或不小于其父结点的值;

                2. 堆总是一棵完全二叉树。

3.3 堆的实现(完整版代码)

        【免费】堆-二叉树的顺序结构-C语言实现资源-CSDN下载

3.3.1 堆结构体声明(同顺序表)

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

3.3.2 初始化&销毁(同顺序表)

// 堆的构建
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->_a = NULL;
	hp->_size = hp->_capacity = 0;
}

// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->_a);
	hp->_a = NULL;
	hp->_size = hp->_capacity = 0;
}

3.3.3 向上调整

        向上调整算法:通过从最后一个结点开始的向上调整算法可以把一个数组调整成一个小堆(大堆)。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

        建小堆:把新增的child和parent比较,如果child<parent就交换,逐层调整。

        建大堆:把新增的child和parent比较,如果child>parent就交换,逐层调整。

        (小堆:根小于子节点,大堆:根大于子节点)

// 交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
// 向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//if (a[child] > a[parent])	// 建大堆,把大的child往上放
		if (a[child] < a[parent])	// 建小堆,把小的child往上放
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

3.3.4 插入(同顺序表尾插)

        堆的插入同顺序表的尾插,只是比顺序表尾插多了一步向上调整。

// 插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->_size == hp->_capacity)
	{
		int newcapacity = hp->_capacity == 0 ? 4 : 2 * (hp->_capacity);
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		hp->_a = tmp;
		hp->_capacity = newcapacity;
	}
	hp->_a[hp->_size++] = x;
	AdjustUp(hp->_a, hp->_size - 1);
}

3.3.5 向下调整

        通过从根结点开始的向下调整算法可以把一个数组调整成一个小堆(大堆)。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

        建小堆:取左右子节点的较小值和根节点比较并交换,逐层调整。

        建大堆:取左右子节点的较大值和根节点比较并交换,逐层调整。

        (小堆:根小于子节点,大堆:根大于子节点)

// 举例
int array[] = {27,15,19,18,28,34,65,49,25,37};

// 向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//if (child + 1 < n && a[child + 1] > a[child]) // 建大堆,把大的放上去。取左右子节点的较大值
		if (child + 1 < n && a[child + 1] < a[child]) // 建小堆,把小的放上去。取左右子节点的较小值
		{
			++child;
		}
		//if (a[child] > a[parent])	// 建大堆。将child较大值和parent交换,逐层调整,再调整成堆
		if (a[child] < a[parent])	// 建小堆。将child较小值和parent交换,逐层调整,再调整成堆
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

3.3.6 删除

         堆删除是删除堆顶的数据将堆顶的数据和最后一个数据交换,然后删除数组最后一个数据,再对堆顶向下调整。(此时满足向下调整条件:左右子树都是堆)

// 删除
void HeapPop(Heap* hp)
{
	assert(hp && hp->_size > 0);
	Swap(&(hp->_a[0]), &(hp->_a[hp->_size - 1]));
	hp->_size--;
	AdjustDown(hp->_a, hp->_size, 0);
}

3.3.7 取堆顶&判空

// 取堆顶数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp && hp->_size > 0);
	return hp->_a[0];
}

// 判空
bool HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->_size == 0;
}

3.4 数组直接调整成堆(向下调整)

        下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过向下调整算法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆

int a[] = {1,5,3,8,7,6};

HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);// 向下调整建堆
	}

	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(a, i);	// 向上调整建堆
	//}

	// 向下调整排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

3.5 建堆时间复杂度(详细证明)

        因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果):

        以下分别是向下调整时间复杂度分析和向上调整时间复杂度分析,由结果得:向下调整建堆优于向上调整建堆。

4. 堆的应用

4.1 堆排序

        堆排序即利用堆的思想来进行排序,总共分为两个步骤:

        1. 建堆

                升序:建大堆

                降序:建小堆

        2. 利用堆删除思想来进行排序

        建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。向下调整建堆+向下调整堆排序时间复杂度 O(NlogN)

// 堆排序
HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);// 向下调整建堆
	}

	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(a, i);	// 向上调整建堆
	//}

	// 向下调整排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
void Test02()
{
	int a[] = { 3,6,5,2,8,4,9,7,1,0 };
	int sz = sizeof(a) / sizeof(a[0]);
	HeapSort(a, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

4.2 TOP-K问题

        TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

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

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

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

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

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

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

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

        代码实现:随机数生成+取100000个数中最大的前十个

// 构造随机数
void CreatData()
{
	int n = 100000;
	srand((unsigned int)time(NULL));
	const char* file = "data.txt";;
	FILE* pf = fopen(file, "w");
	if (pf == NULL)
	{
		perror("fopen fail");
		return;
	}
	for (int i = 0; i < n; i++)
	{
		int x = (rand() + i) % 10000000;
		fprintf(pf, "%d\n", x);
	}
	fclose(pf);
}

// top(k)问题
void Test03()
{
	int k;
	printf("请输入k>:");
	scanf("%d", &k);
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc fail");
		return;
	}

	const char* file = "data.txt";
	FILE* pf = fopen(file, "r");
	if (pf == NULL)
	{
		perror("fopen fail");
		return;
	}

	// 读取文件前k个数
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", &kminheap[i]);
	}
	// 建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(kminheap, k, i);
	}
	int x = 0;
	while (fscanf(pf, "%d", &x) != EOF)
	{
		if (x > kminheap[0])
		{
			kminheap[0] = x;
			AdjustDown(kminheap, k, 0);
		}
	}

	printf("最大的前k个数:");
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
	fclose(pf);

}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

..过云雨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值