堆的结构实现与应用

news2024/11/18 18:22:40

目录

         前言:

1.认识堆

a.如何认识堆?

b.大根堆与小根堆

c.堆应用的简单认识

2.堆的结构与要实现的功能

3.向上调整算法

4.向下调整算法

5.向堆插入数据并建堆

6.堆的大小

7.堆的判空

8.取堆顶数据

9.删除堆顶数据

10.向上调整时间复杂度

11.向下调整时间复杂度

12.堆排序

a.直接将数组放到堆再取堆顶

b.在将数组放到堆的时候就直接调整,用数组建堆

13.topk问题

总结:


前言:

堆其实与二叉树息息相关,本篇将从如何实现堆,以及堆的应用等方面入手。

1.认识堆

a.如何认识堆?

我们只要记住关键的两点:1.堆必须是完全二叉树。2.堆要么是大堆,要么是小堆。

b.大根堆与小根堆

那什么是大堆,什么又是小堆呢?

大堆:树中任意一个父亲都大于或等于孩子。

小堆:树中任意一个父亲都小于或等于孩子。

c.堆应用的简单认识

堆排序:时间复杂度为O(N*logN),属于快一点的排序。

topk问题:N个数找最大的前K个。

优先级队列:C++中stl的priority_queue容器的底层实现需要用到建堆的思想。

2.堆的结构与要实现的功能

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

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

void InitHeap(HP* php);
void DestroyHeap(HP* php);
void PushHeap(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(int* a, int n, int parent);
void Swap(HPDataType* p1, HPDataType* p2);

我们提供以下数据来建堆:

3.向上调整算法

我们现在如果要让提供的数据插入到这个堆里面,如何保证插入的时候就建好堆了呢?这时我们就要用到向上向下调整的算法了。假设我们现在要建一个小堆,就要保证每一个子节点都要小于或者等于它的父节点,用我们提供好的数据,当插入到32这个数据的时候就要进行调整了:

既然是向上调整,那我们就要找孩子的父亲,那如何找到父亲呢?通过下标的关系可以发现,不管是左还是右孩子,只要遵循(child-1)/2就能找到父亲的下标,然后就是交换嘛;交换过后我们要让孩子走到父亲位置,再找到新的父亲,一轮一轮的向上,这就是向上调整算法:

void AdjustUp(HPDataType* a, int child)
{
	//默认建小堆
	int parent = (child - 1) / 2;

	while (child > 0)//等于0就停止了,等于0说明孩子在根的位置,就没有父亲了
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);

			child = parent;//孩子走到父亲的位置继续再找新的父亲
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

要注意的就是循环条件,如果孩子走到0说明走到根的位置了,就没有父亲了,循环停止。

4.向下调整算法

向下调整算法与向上调整算法相反,其实向下就是找孩子,如果建小堆就让左右孩子中最小的孩子与父亲交换(最小的孩子与父亲交换后,父亲就变成三者中最小的了,符合小堆的性质),再让父亲走到孩子的位置上,再往下继续找到新的孩子,直到孩子不存在的情况。

关键来了,如何找到左右孩子中的最小的那个呢?如何通过父亲找到孩子呢?

我们可以默认左孩子是最小的那一个,如果左孩子大于右孩子,那做孩子的下标+1不就到右孩子了吗?解决了第一个问题,那如何通过父亲找到孩子呢,既然我们默认左孩子是小的那一个,我们可以先找到左孩子,通过下标的关系,我们就知道左孩子child=parent*2+1,好了,这就是向下调整算法的思路,来看代码:

void AdjustDown(int* a, int n, int parent)
{
	//默认小堆
	int child = parent * 2 + 1;//默认是左孩子

	while (child < n)
	{
		//这里右孩子的存在条件必须放在&&的前面,因为如果放在后面,前面的条件为假,右孩子也为假,就判断不出来是哪个了(检查右孩子存在必须更严格)
		if (child + 1 < n && a[child] > a[child + 1])//如果右孩子存在(因为如果左孩子为n-1,那右孩子就为n了,就越界了)并且左孩子大于右孩子,下标就走到右孩子上
		{
			child = child + 1;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);

			parent = child;
			child = parent * 2 + 1;
		}

		else
		{
			break;
		}
	}

}

注意点:

1.我们既然默认左孩子为小的那一个,那结束条件就应该是左孩子不存在的情况即当左孩子等于n的时候就越界了,而又由于堆是完全二叉树,所以左孩子不存在,那右孩子一定不存在,所以只写这一个就行。

2.child + 1 < n && a[child] > a[child + 1],首先需要注意左孩子存在,但右孩子不存在的情况,所以判断child+1<n,其次这个条件要写到&&的前面,因为如果写到后面,a[child]>a[child+1]为假,就判断不出右孩子可能越界的情况了,所以右孩子的检查应该放到&&前面。

5.向堆插入数据并建堆

void PushHeap(HP* 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, sizeof(HPDataType) * NewCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity = NewCapacity;

	}
	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);//向上调整传孩子,即插入的数据,找父亲

}

需要注意的是最后向上调整传的孩子是插入的数据的下标,因为插入后size++了,所以-1才对应插入的数据的下标。

6.堆的大小

int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

7.堆的判空

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

8.取堆顶数据

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(&php));

	return php->a[0];
}

9.删除堆顶数据

如果我们直接删除堆顶的数据会导致这个堆变乱,所以我们采用交换堆顶和堆尾的数据,将堆的大小减1,这样就访问不到堆尾的数据也就起到了删除的效果了,然后我们再从根节点开始做向下调整算法,恢复堆即可,注意空堆不能删,要判空:

void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(&php));
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	AdjustDown(php->a, php->size, 0);//从根节点开始向下调整

}

10.向上调整时间复杂度

按最坏的情况计算时间复杂度,就拿满二叉树来说:

我们先找到每一次的节点数,再乘向上调整的次数,假设树的高度为h,那我们就将1-h层的所有节点的调整次数相加,就是时间复杂度(计算采用等比数列求和的乘公比错位相减的方法):

又因为满二叉树的节点个数为2^h-1,所以我们设树有N个节点,就能得到高度,再代入F(h):

实际去除不影响结果的项也就是O(N*logN),N为节点个数。

11.向下调整时间复杂度

一样拿满二叉树来说:

实际计算结果:

实际去除不影响结果的项也就是O(N),N为节点个数。

12.堆排序

a.直接将数组放到堆再取堆顶

void HeapSort1(int* a, int n)
{
	HP hp;
	InitHeap(&hp);


	for (int i = 0; i < n; i++)
	{
		PushHeap(&hp, a[i]);
	}

	int i = 0;
	while (!HeapEmpty(&hp))
	{
		int top = HeapTop(&hp);
		a[i++] = top;
		HeapPop(&hp);
	}
}

这样的坏处就是想要改升序降序要改向上向下调整的逻辑,有些麻烦,而且时间上有些麻烦,需要堆排的时候还要写一个堆出来。

b.在将数组放到堆的时候就直接调整,用数组建堆

如果我们要排成降序,就建小堆,小堆选出最小的,首尾交换,最小的放到最后的位置,最后一个数据不看做堆里面的,再次向下调整就可以选出次小的,以此类推,相当于一个一个头插;

调用一次是O(logN),N次就是O(N*logN),计算方法跟向下调整差不多;

向下调整建堆需要倒着调整,叶子节点不需要处理,倒数第一个非叶子节点即最后一个节点的父亲开始调整:

void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

如果我们要排升序,只需要改动向下调整或者写两个建堆方法:

改动两处:选孩子时选大的那个;如果孩子大于父亲,交换:

建堆时也可向上调整建堆,具体实现博主暂时不清楚~~~:

for (int i = 1; i < n; i++)//下标为0即第一个数默认是堆
	{
		AdjustUp(a, i);//建堆,相当于一个一个插入成堆
	}

注意:

1.为什么升序不建小堆呢?因为小堆最小的已经在前面了,不管是移动还是怎么剩下的都要重新建堆

2.堆排序整体时间复杂度为N+N*logN,也就是O(N*logN)

13.topk问题

什么是topk问题?

就是N个数找最大的前N个:

面对庞大的数据,数据放在磁盘的文件里面,而内存是有限的,所以我们将这些数据的前k个建堆,将剩下的数据与堆顶元素进行比较,符合条件就交换,然后再调整,重复操作即可,那该怎么建堆呢?首先对前3 数据进行建一个小堆,注意这里不能建大堆,如果建大堆的话,可能最大的数据在前三个数,其余2个数据在余下的 N-K个数里面,这样其余2个就不能进堆了:

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

	fclose(fin);
}

void PrintTopK(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc error");
		return;
	}
	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 val = 0;
	while (!feof(fout))//fscanf读到文件结尾,调用feof,feof读到文件末尾返回非0,否则返回0
	{
		fscanf(fout, "%d", &val);
		if (val > kminheap[0])
		{
			kminheap[0] = val;
			AdjustDown(kminheap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
}

总结:

堆的结构不难,难在和其他的场景联系到一起并涉及一些算法,所以还是掌握好它结构和算法的基础为主

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

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

相关文章

Js如何判断两个数组是否相等?

本文目录 1、通过数组自带方法比较2、通过循环判断3、toString()4、join()5、JSON.stringify() 日常开发&#xff0c;时不时会遇到需要判定2个数组是否相等的情况&#xff0c;需要实现考虑的场景有&#xff1a; 先判断长度&#xff0c;长度不等必然不等元素位置其他情况考虑 1…

jmeter下载base64加密版pdf文件

一、何为base64加密版pdf文件 如下图所示&#xff0c;接口jmeter执行后&#xff0c;返回一串包含大小写英文字母、数字、、/、的长字符串&#xff0c;直接另存为pdf文件后&#xff0c;文件有大小&#xff0c;但是打不开&#xff1b;另存为doc文件后&#xff0c;打开可以看到和…

Docker技术仓库

数据卷 为什么用数据卷&#xff1f; 宿主机无法直接访问容器中的文件容器中的文件没有持久化&#xff0c;导致容器删除后&#xff0c;文件数据也随之消失容器之间也无法直接访问互相的文件 为解决这些问题&#xff0c;docker加入了数据卷机制&#xff0c;能很好解决上面问题…

MT8788|MTK8788安卓核心板参数_4G联发科MTK模块

MT8788核心板是一款功能强大的4G全网通安卓智能模块。该模块采用了联发科AIOT芯片平台&#xff0c;具有长达8年的生命周期。MT8788模块内置了12nm制程的八核处理器&#xff0c;包括4个Cortex A73和4个Coretex A53&#xff0c;主频最高可达2.0GHZ。标配内存为4GB64GB&#xff0c…

Github 2024-02-22 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-22统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目4非开发语言项目2Go项目2HTML项目1Dart项目1Vue项目1JavaScript项目1TypeScript项目1 《Hello 算法…

HarmonyOS开发技术全面分析

系统定义 HarmonyOS 是一款 “ 面向未来 ” 、面向全场景&#xff08;移动办公、运动健康、社交通信、媒体娱乐等&#xff09;的分布式操作系统。在传统的单设备系统能力的基础上&#xff0c;HarmonyOS提出了基于同一套系统能力、适配多种终端形态的分布式理念&#xff0c;能够…

node版本管理工具之nvm的安装和使用

一、nvm的介绍 1&#xff0c;什么是nvm&#xff1f; nvm是Node Version Manager的简称&#xff0c;即为node版本管理工具 2&#xff0c;为什么要选择使用nvm&#xff1f; nodejs使用的场景越来越多&#xff0c;因为是开源软件&#xff0c;具备大多数开源软件都存在的"…

测试多线程架构的问题

在测试多线程架构时&#xff0c;需要考虑多个方面以确保系统的稳定性和性能。以下是一些关键问题&#xff0c;需要在测试过程中特别关注&#xff1a; 线程同步 多线程环境中&#xff0c;线程同步是非常重要的问题。由于多个线程可能同时访问共享资源&#xff0c;因此需要使用…

启动node服务报错Error: listen EACCES: permission denied 0.0.0.0:5000

启动node服务报错&#xff1a; 解决方案&#xff1a; 将监听端口改成3000或者其他 修改后结果&#xff1a; 参考原文&#xff1a; Error: listen EACCES: permission denied_error when starting dev server: error: listen eacc-CSDN博客

git版本回退在eclipse和命令中的操作

一.背景 老程序员了&#xff0c;熟悉eclipsesvn&#xff0c;git用的不溜。近几年用了git&#xff0c;偶尔修改了某个文件希望放弃本次修改重新恢复到最新版本重新修改。或者回退到某个版本&#xff0c;再修改。记录一下Eclipse中的操作&#xff0c;和命令操作的情况。 二.Ecli…

六、回归与聚类算法 - 线性回归

目录 1、线性回归的原理 1.1 应用场景 1.2 什么是线性回归 1.2.1 定义 1.2.2 线性回归的特征与目标的关系分析 2、线性回归的损失和优化原理 2.1 损失函数 2.2 优化算法 2.2.1 正规方程 2.2.2 梯度下降 3、线性回归API 4、回归性能评估 5、波士顿房价预测 5.1 流…

Java 注解机制解密并发编程的时间之谜:揭开Happens-Before的神秘面纱

优质博文&#xff1a;IT-BLOG-CN 一、简介 为什么需要happens-before原则&#xff1a; 主要是因为Java内存模型 &#xff0c; 为了提高CPU效率&#xff0c;通过工作内存Cache代替了主内存。修改这个临界资源会更新work memory但并不一定立刻刷到主存中。通常JMM会将编写的代码…

2024.4.21

多进程实现拷贝 #include <myhead.h> //定义结构体 typedef struct INFO {const char *src_file;const char *dest_file;int mv;int size;}info_t;//获取源文件的大小并且创建目标文件 int size_creat(const char *src_file,const char *dest_file) {//获取源文件的大小…

Linux多线程服务端编程:使用muduo C++网络库 学习笔记 第十二章 C++经验谈(一)

作者对C的基本态度是“练从难处练&#xff0c;用从易处用”&#xff0c;因此本章有几节“负面”的内容。作者坚信软件开发一定要时刻注意减少不必要的复杂度&#xff0c;一些花团锦簇的招式玩不好反倒会伤到自己。作为应用程序的开发者&#xff0c;对技术的运用要明智&#xff…

说一说Eclipse的项目类型和常用项目的区别

Eclipse在新建项目的时候有很多类型&#xff0c;包括Java project、Web project等等&#xff0c;如下&#xff1a; 那么这些项目类型有什么区别呢&#xff1f;我们在创建项目的时候应该如何选择&#xff0c;了解清楚这一点还是非常重要的&#xff0c;但记住一个出发点&#xff…

Simulink算法仿真注意事项

一、背景 首先&#xff0c;需要区分Simulink仿真和FPGA仿真的区别&#xff0c;即使最终算法可能要落地到FPGA上。 Simulink是MATLAB的一个重要组成部分&#xff0c;主要用于建模、仿真等&#xff0c;它可将理论研究和实践有机的相结合&#xff0c;并且用户不需要书写大量的代…

⭐北邮复试刷题LCR 037. 行星碰撞__栈 (力扣119经典题变种挑战)

LCR 037. 行星碰撞 给定一个整数数组 asteroids&#xff0c;表示在同一行的小行星。 对于数组中的每一个元素&#xff0c;其绝对值表示小行星的大小&#xff0c;正负表示小行星的移动方向&#xff08;正表示向右移动&#xff0c;负表示向左移动&#xff09;。每一颗小行星以相…

Elasticsearch:使用 ELSER v2 进行语义搜索

在我之前的文章 “Elasticsearch&#xff1a;使用 ELSER 进行语义搜索”&#xff0c;我们展示了如何使用 ELESR v1 来进行语义搜索。在使用 ELSER 之前&#xff0c;我们必须注意的是&#xff1a; 重要&#xff1a;虽然 ELSER V2 已正式发布&#xff0c;但 ELSER V1 仍处于 [预览…

基于Jenkins实现的CI/CD方案

基于Jenkins实现的CI/CD方案 前言 最近基于Jenkins的基座&#xff0c;搭建了一套适用于我们项目小组的持续集成环境。现在把流程整理分享出来&#xff0c;希望可以给大家提供一些帮助和思路。 使用到的组件和版本 组件名称组件版本作用Harbor2.7.3镜像仓库Jenkins2.319.2持…

【鸿蒙系统学习笔记】网络请求

一、介绍 资料来自官网&#xff1a;文档中心 网络管理模块主要提供以下功能&#xff1a; HTTP数据请求&#xff1a;通过HTTP发起一个数据请求。WebSocket连接&#xff1a;使用WebSocket建立服务器与客户端的双向连接。Socket连接&#xff1a;通过Socket进行数据传输。 日常…