【数据结构】第七节:堆

news2024/10/6 13:18:00

 个人主页 深情秋刀鱼@-CSDN博客 

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

源码获取:数据结构: 上传我写的关于数据结构的代码 (gitee.com)

目录

一、堆

1.堆的概念

2.堆的定义

二、堆的实现

1.初始化和销毁

2.插入

向上调整算法

3.删除

向下调整算法

4.取堆顶元素

5.判空

三、Top_k问题

1.问题描述

2.面试中的Top_k问题

四、堆排序

1.建堆

 2.堆排序

五、堆的时间复杂度

1.建堆

a.树中高度与节点的关系

b.向下调整建堆算法

c.向上调整建堆算法

 2.堆排序


一、堆

1.堆的概念

         堆是一棵完全二叉树,且其中的节点总是不大于(或不小于某个值)。如果堆中的节点总是不大于某个值(根节点最大),称为大根堆;如果堆中的节点总是不小于某个值(根节点最小)将根节点最小的堆称为小根堆。

        大根堆和小根堆描述的是双亲节点和子节点之间的关系,而子节点之间没有直接的联系。

2.堆的定义

typedef int HPDataType;

//堆
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;

        二叉树一般可以使用两种结构存储,一种顺序结构(数组),一种链式结构(链表)。由于堆是一棵完全二叉树,用数组结构存储较为简洁。

        数组中双亲节点和子节点之间的关系:

  • 当双亲结点的下标为i时,左子节点的下标=2 * i + 1,右子节点的下标=2 * i + 2
  • 当子节点的下标为i时,双亲节点的下标=(i - 1)/ 2

二、堆的实现

1.初始化和销毁

//初始化
void HPInit(Heap* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

//销毁
void HPDestroy(Heap* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

2.插入

        堆在内存中是以数组的形式存储的,在逻辑上需要将数组看成一棵完全二叉树。向堆中插入数据时要保证堆的结构不被破坏,并将其调整为小根堆或大根堆时需要用到向上调整算法

向上调整算法

        使用前提:左右子树必须是一个堆,才能调整。

        算法实现:以小根堆为例,在数组尾部(下标为size-1)的位置插入数据(记下标为child),被插入数据child通过下标之间的关系找到child所在的这棵子树的根(记下标为parent)并与根节点比较,如果a[child]<a[parent]说明此时双亲节点大于子结点的,不符合小根堆的性质,此时需要交换child与parent的位置并更新child和parent的值,一直到堆顶(下标为0)则调整结束。

//交换
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType tmp = *b;
	*b = *a;
	*a = tmp;
}

//向上调整算法(小根堆)
void AdjustUP(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;//更新下标值
		}
		else
			break;
	}
}
  •  图解(小根堆):

  • 代码实现:
//插入
void HPPush(Heap* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size++] = x;
	AdjustUP(php->a, php->size - 1);
}

3.删除

        删除规定只删除堆顶元素(删除堆尾元素size--即可),删除堆顶元素的同时需要保持结构不变,需要用到向下调整算法。

向下调整算法

        使用前提:左右子树必须是一个堆,才能调整。

        算法实现:以小根堆为例,将首(B)尾(A)元素交换,在尾部删除堆顶元素B,在堆顶的尾元素A通过向下调整算法调整到合适的位置再形成堆。新的堆顶元素A下标为0(记为parent),以parent为根的两个子节点分别为左child节点(下标2*parent+1)、右child节点(下标2*parent+2),为满足小根堆的性质,我们需要在这两个节点中找到较小的一个与元素A交换成为新的根,元素A成为子节点后再向下寻找以元素A为根的两个子节点,一直到堆底调整结束。

//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{
    //假设法
	int child = 2 * parent + 1;
	while (child < n)
	{
        //两个子节点中较小的那个(注意边界的处理)
		if (child + 1 < n && a[child] > a[child + 1])
			child++;
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			break;
	}
}

        在判断两个子节点的大小时不妨先假设左子节点大,进入循环后再判断左右子节点的大小。

  • 图解(小根堆): 

  •  代码实现:
//删除(删除堆顶的数据)
void HPPop(Heap* php)
{
	assert(php && php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

4.取堆顶元素

//取堆顶
HPDataType HPTop(Heap* php)
{
	assert(php && php->size > 0);
	return php->a[0];
}

5.判空

//判空
bool HPEmpty(Heap* php)
{
	assert(php);
	return php->size == 0;
}

三、Top_k问题

1.问题描述

        TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
        对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

        简单来说:以求取数组中前六个最大的元素为例,将一个元素个数为n的数组调整为大堆后,堆顶的元素就是数组中n个元素的最大值,获取堆顶元素后将堆顶元素删除(删除的步骤是堆顶元素先与堆尾元素交换,在堆尾删除堆顶元素),通过向下调整算法调整堆的结构使其仍然呈大堆排列,排列之后新的堆顶元素就是数组中n-1个元素中的最大值,依此类推。

  • 代码实现:
int a[] = { 1,2,3,5,4,9 };
int k;//前k个
scanf_s("%d", &k);
while (k--)
{
	printf("%d ", HPTop(&hp));
	HPPop(&hp);
}
  •  运行结果

2.面试中的Top_k问题

C语言:文件操作详解-CSDN博客

  • 给出N个整数,存储在磁盘文件中,要求取出最大的前k个元素。

        这个问题属于最常规的Top_k问题,建大堆然后依次popk个元素即可。但是面试中往往不会这么简单,这种方法固然存在一定的缺陷:当N过于大时,占用内存空间较多,如果给出10亿个整数就需要占用将近4G的内存空间,如果面试官对内存空间做出限制,显然这种方法就行不通了。

  • 给出N个整数,存储在磁盘文件中,要求取出最大的前k个元素且占用的内存空间不允许超过1KB。

        介绍一种很巧妙的方法:取前k个元素建小堆,然后用剩下的N-k个元素与堆顶元素比较,如果大于堆顶元素则直接覆盖堆顶元素,成为新的堆顶元素,最后用向下调整算法调整结构,依次遍历完所有的数据。这样留在堆中的元素就是最大的前k个元素。

//在text中创建N个数据
void CreateN()
{
	int n;
	scanf("%d", &n);
	srand((unsigned int)time(0));
	const char* FileName = "D:\\Git code\\data-structure\\Project_Heap\\Project_Heap\\data.txt";//文件地址
	FILE* fin = fopen(FileName, "w");
	if (fin == NULL) {
		perror("fopen fail");
		return;
	}
	for (int i = 1; i <= n; i++) {
		int x = (rand() + i) % 10000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

//Top_k
void Test3()
{
	CreateN();
	const char* FileName = "D:\\Git code\\data-structure\\Project_Heap\\Project_Heap\\data.txt";
	FILE* fout = fopen(FileName, "r");
	int k;
	scanf("%d", &k);
	int* kMinHeap = (int*)malloc(sizeof(int) * k);
	if (kMinHeap == NULL) {
		perror("malloc fail");
		return;
	}
	//将文件中的数据(前k个)读取到数组中
	for (int i = 0; i < k; i++) {
		fscanf(fout, "%d", &kMinHeap[i]);
	}
	//建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(kMinHeap, k, i);
	}
	int x;
	while (fscanf(fout, "%d", &x) > 0) {
		if (x > kMinHeap[0]) {
			kMinHeap[0] = x;
			AdjustDown(kMinHeap, k, 0);
		}
	}
	for (int i = 0; i < k; i++) {
		printf("%d\n", kMinHeap[i]);
	}
	fclose(fout);
}

四、堆排序

1.建堆

        给定一个数组,要求将其调整为大堆或小堆。我们可以将原数组直接看成一棵完全二叉树,然后利用向上或向下调整算法将其调整为大堆或小堆,大堆和小堆是可以自由切换的,只需要更改向下和向上调整算法中的比较逻辑即可。

  • 向上调整算法建小堆
int a[] = { 2,3,1,4,6,5,9 };
Heap hp;
HPInit(&hp);
int n = sizeof(a)/xizeof(int);
for (int i = 1; i < n; i++)
	AdjustUP(a, i);

        建堆逻辑:总是保证前i个数据具有堆的性质,当i=n时,整棵树都具有了堆的性质。


  • 向下调整算法建小堆
int a[] = { 2,3,1,4,6,5,9 };
Heap hp;
HPInit(&hp);
int n = sizeof(a)/sizeof(int);
for (int i = (n - 1 - 1) / 2; i < n; i++)
	AdjustDown(a, n, i);

        在向下调整算法建堆时,我们从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆。

        建堆逻辑:总是保持后i个数据具有堆的性质,当i=0时,整棵树都具有了堆的性质。


  • 图解(大根堆):

 2.堆排序

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

1. 建堆
  • 升序:建大堆
  • 降序:建小堆
2. 利用堆删除思想来进行排序:建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
        算法逻辑:以降序建小堆为例,一个元素个数为n的数组调整为小堆后,堆顶元素是数组中的最小元素,将堆顶元素与堆尾交换,然后对新的堆顶元素向下调整,一直调整到合适的位置再形成堆,此时堆顶元素应为数组中次小的元素,将次小的元素与第n-1个元素(倒数第二个)交换,再利用向下调整算法调整结构,依此类推。
        代码实现:
//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = 2 * parent + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] > a[child + 1])
			child++;
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
			break;
	}
}

//交换
void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType tmp = *b;
	*b = *a;
	*a = tmp;
}


//堆排序(O(N*logN))
void HPSort(HPDataType* a, int n)
{
	//降序:建小堆
	//升序:建大堆
	//for (int i = 1; i < n; i++)
	//	AdjustUP(a, i);//向上调整建堆
	for (int i = (n - 1 - 1) / 2; i < n; i++)
		AdjustDown(a, n, i);
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

图解(升序大根堆):

五、堆的时间复杂度

1.建堆

a.树中高度与节点的关系

        设有一棵高度为h的满二叉树,如下图:

根据递推公式我们可以得到节点N与高度h的关系:F(h)=2^0+2^1+2^2+.....+2^(h-1)。根据等比数列求和公式,F(h)=2^h-1。


        一棵完全二叉树节点最多的情况是一棵满二叉树(最后一层全满),节点最少的情况是最后一层有且仅有一个节点的情况。

  • 满二叉树:F(h)=2^h-1=N——h=log(N+1)
  • 完全二叉树节点最少情况:F(h)=2^(h-1)=N——h=logN+1

        综上完全二叉树的节点应在log(N+1)与logN+1之间,根据大O的渐进表示法为logN。


b.向下调整建堆算法

        在向下调整建堆的过程中,我们选择从最后一个非叶子节点的节点开始调整,在计算时间复杂度时,只考虑最坏的情况,将堆简化看作一棵满二叉树,即每个双亲节点都需要调整到最底部,如第一层2^0个节点向下移动4次,第二层2^1个节点向下移动2层,第三层2^2个节点向下移动1次。

综上,向下调整建堆得时间复杂度为O(N)。

c.向上调整建堆算法

        在向上调整建堆中,我们选择从第一个子节点开始调整。还是只考虑最坏的情况并将堆简化为一棵满二叉树。从第2个节点开始,每个节点都需要向上调整高度次,即第二层2^1个节点向上移动1次,第三层2^2个节点向上移动2次,第四层2^3个节点(看作满二叉树)向上移动3次。

综上,向下调整建堆得时间复杂度为O(N*logN)。

 2.堆排序

建堆时间复杂度对比:

  • 向上调整建堆:O(N*logN)
  • 向下调整建堆:O(N)

堆排序的实现:

void HPSort(HPDataType* a, int n)
{
	//降序:建小堆
	//升序:建大堆
	//for (int i = 1; i < n; i++)
	//	AdjustUP(a, i);//向上调整建堆
	for (int i = (n - 1 - 1) / 2; i < n; i++)
		AdjustDown(a, n, i);
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

        建堆结束后,类比调整算法的推导可以得出排序的时间复杂度是O(N*logN)。  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1689077.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

短剧系统源码解析与应用

在数字化时代&#xff0c;短剧作为一种新兴的娱乐形式&#xff0c;因其内容紧凑、节奏快速而受到广大年轻群体的喜爱。短剧系统源码的开发和应用&#xff0c;不仅为创作者提供了一个展示才华的平台&#xff0c;也为观众带来了全新的观看体验。本文将对短剧系统源码进行解析&…

get和post的区别,二者是幂等的吗?

一、什么是幂等 所谓幂等性通俗的将就是一次请求和多次请求同一个资源产生相同的副作用。 维基百科定义&#xff1a;幂等&#xff08;idempotent、idempotence&#xff09;是一个数学与计算机学概念&#xff0c;常见于抽象代数中。 在编程中一个幂等操作的特点是其任意多次执…

win32-鼠标消息、键盘消息、计时器消息、菜单资源

承接前文&#xff1a; win32窗口编程windows 开发基础win32-注册窗口类、创建窗口win32-显示窗口、消息循环、消息队列 本文目录 键盘消息键盘消息的分类WM_CHAR 字符消息 鼠标消息鼠标消息附带信息 定时器消息 WM_TIMER创建销毁定时器 菜单资源资源相关菜单资源使用命令消息的…

人类交互2 听觉处理和语言中枢

人类听觉概述 人类听觉是指通过耳朵接收声音并将其转化为神经信号&#xff0c;从而使我们能够感知和理解声音信息的能力。听觉是人类五种感觉之一&#xff0c;对我们的日常生活和交流至关重要。 听觉是人类交流和沟通的重要工具。通过听觉&#xff0c;我们能够听到他人的语言…

jwtcracker下载安装出现错误

1.jwtcracker 用于爆破jwt秘钥 2.下载 ubuntu/kali安装c-jwt-cracker及使用方法-CSDN博客 参考这个大佬写的 但是我在这里出现了这个问题 显示Cannot initialize the default message digest sha256, aborting 我实在找不出来哪里有问题&#xff0c;所以直接换成docker …

Java并发面试题,多线程通关秘籍

【知识点记录】- 不能不知道的知识点 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &#x1f31d;分享学习心得&#xf…

Go语言(Golang)的开发框架

在Go语言&#xff08;Golang&#xff09;的开发中&#xff0c;有多种开发框架可供选择&#xff0c;它们各自具有不同的特点和优势。以下是一些流行的Go语言开发框架&#xff0c;选择Go语言的开发框架时&#xff0c;需要考虑项目需求、团队熟悉度、社区支持、框架性能和可维护性…

Android 共享内存

Parcelable 和 Serializable 区别 Serializable IO完成&#xff08;通过磁盘文件读写&#xff09; Parcelable C 对象指针 来实现共享内存 import android.os.Parcel; import androidx.annotation.NonNull;public class ApiResponseBean extends Throwable implements Parce…

小短片创作-组装场景(一)

1、项目基础设置 通过第三人称模板&#xff0c;创建1个项目 1.自动曝光&#xff1a;关闭&#xff0c;因为要做专业的小短片&#xff0c;曝光需要手动控制。 2.扩展自动曝光中的默认亮度范围&#xff1a;启用 3.全局光照系统&#xff1a;选择屏幕空间光照&#xff08;SSGI&am…

SpringCloudAlibaba:6.3SpringBoot接入RocketMQ

依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 htt…

vue通过for循环生成input框后双向绑定失效问题

有些时候页面上有太多的表单元素&#xff0c;一个个的写太过繁琐&#xff0c;拿 input 框举例&#xff0c;众多的 input 框&#xff0c;无非就是输入框前的说明和 input 框的 name 属性不一样 <el-form :inline"true" :model"formInline" size"mi…

C语言-atoi()库函数的模拟实现

文章目录 前言一、atoi()库函数的介绍及使用1.1 atoi()库函数介绍1.2 atoi()库函数使用 二、atoi()库函数的模拟实现2.1 函数设计2.2 函数实现思路2.3 具体实现2.4 测试 总结 前言 本篇文章介绍c语言中库函数atoi()的使用&#xff0c;以及模拟实现库函数。 一、atoi()库函数的…

allegro 无法删除Xnet

allegro 无法删除Xnet Orcad中打开Constraint Manager之后&#xff0c;再生成网表&#xff0c;导入PCB后就会出现一堆Xnet网络。无法去除Xnet。 解决办法 在原理图ORCAD中&#xff0c; 1、打开Edit Object properties 2、选择Filter by:Capture 3、点击New Property 4、设置…

开源博客项目Blog .NET Core源码学习(25:App.Hosting项目结构分析-13)

本文学习并分析App.Hosting项目中后台管理页面的文章管理页面。   文章管理页面用于显示、检索、新建、编辑、删除文章数据&#xff0c;以便在前台页面的首页、文章专栏、文章详情页面显示文章数据。文章管理页面附带一新建及编辑页面&#xff0c;以支撑新建和编辑文章数据。…

5月20日分割等和子集+最后一块石头的重量Ⅱ

416.分割等和子集 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&#xff1a;nums [1,5,11,5] 输出&#xff1a;true 解释&#xff1a;数组可以分割成 [1, 5, 5] 和…

Qt moc系统的黑魔法?

Qt的元对象系统&#xff08;Meta-Object System&#xff09;是Qt框架的核心功能之一&#xff0c;为C语言增加了一些动态特性&#xff0c;借助元对象系统Qt可以实现以下功能 信号与槽机制&#xff08;Signals and Slots&#xff09;运行时类型信息&#xff08;Run-Time Type In…

激光雷达和相机外参标定

1、外参标定标定什么&#xff1f; 标定激光雷达和相机之间的相对位置&#xff0c;变换矩阵T 2、原理 通过激光扫描出来的轮廓和相机识别出来的栅格的轮廓进行PNP匹配。 3、论文

IDEA的妙用

IDEA 安装破解 复制JetbrainsIdesCrack-4.2.jar到安装目录下 修改安装目录下的bin目录的idea64.exe.vmoptions&#xff1a; 最后一行添加&#xff1a;-javaagent:E:\develop\JetBrains\IntelliJ IDEA 2018.3.5\bin\JetbrainsIdesCrack-4.2.jar(注意&#xff1a;使用自己的路…

大模型时代,掌握Event Stream技术提升Web响应速度

大模型时代,每天搜索都可能会用到一种或多种大模型,在大文本输出的时候,页面是一字一字,一段一段的慢慢输出出来的,这背后是如何实现的呢?我们以KIMI为例 先抓个请求 我们发现界面展示是一句话,但是接口返回的时候是一个字一个字的。 普通请求 多了Event Stream的处理 …

OS复习笔记ch5-5

引言 上文我们了解了一些关于信号量机制的一些经典应用&#xff0c;知识点比较繁杂&#xff0c;可能会有点啰嗦&#xff0c;但是对初学者而言还是越详细越好。接下来我们介绍一下管程。 管程 管程是程序设计语言结构&#xff0c;可提供易于控制的同步机制&#xff0c;我们熟…