首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >数据结构与算法——堆

数据结构与算法——堆

作者头像
我不是呆头
发布2025-12-20 10:15:39
发布2025-12-20 10:15:39
980
举报

树的概念与结构

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

  • 有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
  • 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、……、Tm ,其中每⼀个集合Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以0个或多个后继。因此,树是递归定义的。
  • ⼦树是不相交的。
  • 除了根结点外,每个结点有且仅有⼀个⽗结点。
  • ⼀棵N个结点的树有N-1条边。

树的相关术语

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

树的表示

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

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

树形结构实际运用场景

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

二叉树

概念与结构

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

从上图可以看出⼆叉树具备以下特点

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

注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的

在这里插入图片描述
在这里插入图片描述

特殊的二叉树

满二叉树

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

完全二叉树

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二叉树存储结构

顺序结构

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储,需要注意的是这⾥的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段。

链式结构

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

在这里插入图片描述
在这里插入图片描述

实现顺序结构二叉树

堆的概念与结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

堆具有以下性质 • 堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值; • 堆总是⼀棵完全⼆叉树。

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

堆的实现

向上调整算法(插入数据)

将新数据插⼊到数组的尾上,再进⾏向上调整算法,直到满⾜堆。 • 先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后。 • 插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双双亲往上调整到合适位置即可。

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
//交换数据
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//向上调整算法  判断条件:小堆:child < parent      大堆:child > parent
void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0) //child向上调整到祖先结点停止循环
	{
		if (arr[child] > arr[parent])   //判断条件:小堆:child < parent      大堆:child > parent
		{
			swap(&arr[child], &arr[parent]);  
			child = parent;  //向上调整,所以把子结点下标变成父结点下标
			parent = (child - 1) / 2;//向上找父结点
		}
		else {
			break;
		}
	}
}

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

删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进⾏向下调整算法。 向下调整算法有⼀个前提:左右⼦树必须是⼀个堆,才能调整。

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
//判空
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
// 向下调整算法 小堆:parent > child  arr[child] > arr[child + 1] 大堆:parent < child   arr[child] < arr[child + 1]
void AdjustDown(HPDataType* arr, int parent, int n)
{
	int child = parent * 2 + 1;  //左孩子
	while(child < n )    //n为最大结点个数,不能越界
	{ 
		if (arr[child] < arr[child + 1] && child + 1 < n) //演示大堆,拿子孩子中大的能够和父亲比较,且确保有右孩子
		{
			child++;   //左孩子小于右孩子,将child = child + 1,(大堆)大的和父亲比较
		}
		if (arr[child] > arr[parent])
		{
			swap(&arr[child], &arr[parent]); //大于父亲,因为是大堆,进行交换
			parent = child;   //向下调整,所以将父结点变成子结点
			child = parent * 2 + 1; //再向下寻找子结点
		}
		else 
		{
			break;
		}
	}
}
//出堆
void HPPop(HP* php) 
{
	assert(!HPEmpty(php)); //检查堆是否有元素(非空堆)。
	swap(&php->arr[0], &php->arr[php->size - 1]);  //交换最后一个子节点和祖先节点的值
	--php->size;
	//向下调整
	AdjustDown(php->arr, 0 , php->size);
}

堆的应用

堆排序(建堆)

堆顶一定是最值,大堆堆顶是最大值,小堆堆顶是最小值。

代码语言:javascript
复制
void tes02()
{
	HP hp;
	HPInit(&hp);

	HPPush(&hp, 56);
	HPPush(&hp, 10);
	HPPush(&hp, 15);
	HPPush(&hp, 30);
	HPPush(&hp, 70);
	HPPush(&hp, 25);
	HPPrint(&hp);

	while (!HPEmpty(&hp)) //如果堆不为空
	{
		int top = HPTop(&hp); //循环取堆顶元素
		printf("%d ", top); //打印
		HPPop(&hp);//出堆顶元素
	}
	HPDestroy(&hp);
}
在这里插入图片描述
在这里插入图片描述

此处大堆为例,如果堆不为空,循环取堆顶元素,然后打印,然后出堆(出堆会使根结点和最后一个结点值交换,然后删除最后一个结点,因为是大堆,运用向下调整法又找剩余数据中的最大值,直到所有的数据出堆),那这就是用用堆排序的方法吗? 这并不是堆排序,因为这需要我们自己去构建一个数据结构堆,实现需要运用堆的一般操作(初始化,销毁,入堆,出堆等等)。

答案:数组建堆,⾸尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次⼤的数据(让数组变成一个堆的结构)

在这里插入图片描述
在这里插入图片描述
向下调整
代码语言:javascript
复制
1. 建堆操作,向下调整
从最后一棵子树开始调整,然后调整parent--继续调整
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
//堆排序
void HPSort(int* arr, int n)
{
	//建堆,向下调整算法
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)  //初始值就是最后一棵子树,n-1为最后一个子结点,再减1除2就是其父结点也就最后一棵子树开始
	{
		AdjustDown(arr,i,n);
	}
}
代码语言:javascript
复制
2.将堆顶和最后一个数据交换位置,--size,向下调整堆,继续重复该过程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
//堆排序
void HPSort(int* arr, int n)
{
	//建堆,向下调整算法
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)  //初始值就是最后一棵子树,n-1为最后一个子结点,再减1除2就是其父结点也就最后一棵子树开始
	{
		AdjustDown(arr,i,n);
	}

	//堆排序
	int end = n - 1; //最后位置一个结点
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		--end;
	}

}
在这里插入图片描述
在这里插入图片描述
向上调整算法
代码语言:javascript
复制
//建堆,向上调整算法
for (int i = 0; i < n; i++)
{
	AdjustUp(arr,i);
}

总结:排升序:建大堆 排降序:建小堆

建堆复杂度分析
代码语言:javascript
复制
计算向上调整算法建堆时间复杂度
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
计算向下调整算法建堆时间复杂度
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
TOP-K问题

TOP-K问题:即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。 ⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了 (可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:

  • 1)⽤数据集合中前K个元素来建堆 前k个最⼤的元素,则建⼩堆(小堆头结点最小),比堆顶数据大就入堆。 前k个最⼩的元素,则建⼤堆(大堆头结点最大),比堆顶数据小就入堆。
  • 2)⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素 将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元 素

先创建数据

代码语言:javascript
复制
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);
}
  1. fprintf(fin, "%d\n", x);
    • fprintf 是C语言中的一个函数,用于将格式化的数据写入文件。
    • fin 是文件指针,指向要写入的文件。
    • "%d\n" 是格式字符串,表示以十进制整数格式写入数据,后面跟一个换行符。
    • x 是要写入的整数值。
  2. fclose(fin);
    • fclose 是C语言中用于关闭文件的函数。
    • fin 是要关闭的文件指针。
    • 使用完文件后必须关闭,以确保所有数据都写入磁盘并释放系统资源。
  3. (rand() + i) % 1000000
    • rand() 是C语言的随机数生成函数,返回一个伪随机整数。
    • + i 将循环计数器加到随机数上,增加随机性(避免连续调用rand()产生的相关性)。
    • % 1000000 是取模运算,确保结果在0到999999之间(生成六位数以内的随机数)。

这个函数的作用是生成100,000个随机整数(每个在0-999,999之间),每行一个,写入到"data.txt"文件中。

代码语言:javascript
复制
void TopK()
{
	int k = 0;
	printf("请输入K:");
	scanf_s("%d", &k);

	const char* file = "data.txt";
	FILE* fout = fopen(file,"r");
	if (fout == NULL)
	{
		perror("fopen fail!");
		exit(1);
	}
	//找最大的前K个数据,建小堆
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail!");
		exit(2);
	}
	for (int i = 0; i < k; i++)
	{
		fscanf_s(fout, "%d",&minHeap[i]);//保存到申请的数组空间
	}
	//minHeap目前并不是堆结构    所以向下调整建堆(时间复杂度更小)
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap,i,k);
	}
	//建堆后,遍历剩下的n-k个数据,与堆顶进行比较,大于堆顶元素则替换
	int x = 0;//将剩下的数据保存到x里面
	while (fscanf_s(fout, "%d", &x) != EOF)
	{
		//x与堆顶数据比较
		if (x > minHeap[0])
		{
			minHeap[0] = x;//但是此时不是有效堆结构
			AdjustDown(minHeap, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	fclose(fout);
}

int main()
{
	TopK();
	return 0;
}
代码语言:javascript
复制
时间复杂度:O(n) = k + (n − k)log2k
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
    • 树的概念与结构
    • 树的相关术语
    • 树的表示
    • 树形结构实际运用场景
  • 二叉树
    • 概念与结构
    • 特殊的二叉树
      • 满二叉树
      • 完全二叉树
    • 二叉树存储结构
      • 顺序结构
      • 链式结构
  • 实现顺序结构二叉树
    • 堆的概念与结构
    • 堆的实现
      • 向上调整算法(插入数据)
      • 向下调整算法
    • 堆的应用
      • 堆排序(建堆)
      • 建堆复杂度分析
      • TOP-K问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档