数据结构---手撕图解堆的实现和TopK的应用

news2024/11/27 14:39:17

文章目录

  • 重要的概念
  • 树的存储方式
    • 顺序存储
    • 链式存储
  • 堆的概念
  • 堆的实现
    • 向上调整算法
    • 一些实现过程中的技巧
    • 实现搭建堆
    • 实现出堆的操作
    • 向下调整算法
  • 堆排序
  • TopK

重要的概念

要讲到堆,先要说两个关于二叉树的概念

  1. 满二叉树:一个二叉树如果每一层的节点数都是最大值,那么这个二叉树就是满二叉树
    在这里插入图片描述

  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是满二叉树的变形,对于深度为k的树有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1至n的节点
    在这里插入图片描述
    上面所展示的就是满二叉树和完全二叉树

树的存储方式

顺序存储

任何一个数据结构在内存中都要以一定的方式存储起来,那么具体如何存储起来?有下面的规则

首先是顺序存储,也就是用顺序表的形式存储,存储形式如下:

在这里插入图片描述
但很明显,这样的存储对于非完全二叉树来说会造成十分严重的内存浪费

链式存储

链式存储相比起顺序存储各有优势,链式存储的规则如下:

在这里插入图片描述

定义一个结构体,结构体中包含这三个成员,这三个成员就可以包含一个树的所有信息

下面重点介绍的是二叉树的顺序结构是如何实现的

堆的概念

首先要明确,这里的堆和malloc的堆并不是一个意思,前者的意思是一种数据结构,而后者是操作系统的一部分区域

堆是一种完全二叉树,它满足下面的性质

堆中某个节点的值总是不大于或不小于其父节点的值
堆总是一棵完全二叉树

那为什么是不大于或不小于?因为堆也是有划分的,堆分为大堆和小堆

在介绍大堆和小堆之前,先说明堆的顺序存储是如何存储的,以下面这个图为例

在这里插入图片描述

上图是一个完全二叉树,其中二叉树的父亲节点总是小于孩子节点,那么这就是小堆,而在内存中的存储形式如下图所示,存储的时候确实是按照数组存储,顺序遵循由上到下由左到右的顺序进行存储

大堆和上图基本一致,之时父亲节点总是大于孩子节点

堆的实现

那么作为一种数据结构,它会有它自己的用途,下面分析堆是如何实现的

从上述的存储结构可以看出,实际上每一个数组都可以把它看成是一个二叉树,由于堆的特殊性,首要问题就是如何把一个数组中的数字排序满足堆的要求

向上调整算法

这个算法主要是用来进行堆中元素的插入,当插入一个元素,由于大堆/小堆,这个插入的元素可能不符合堆的要求,这时候就需要用到向上调整算法

该算法的应用场景就是当一个元素要插入一个堆时,可以用这个算法进行插入,使得后续的二叉树依旧是堆,前提是插入前的二叉树必须满足堆的要求

该算法的流程是这样的

在这里插入图片描述

首先,原本有一个堆,有一个新元素12要插入这个堆中,它的位置应该是作为15的孩子节点,但由于小堆的规则,12小于15,因此这里的12应该和15交换一下位置,接着12再和它的上一代比较,发现12小于10,满足小堆的规则,因此新堆就变成了右图所示的模样,至此就完成了堆的插入

这里不得不提的是,插入的元素要进行向上调整算法的最终目标是它的祖先,只要它和上一代之间不满足规则,就一直交换,直到它成为了它所在的这一代的祖先为止

一些实现过程中的技巧

知道了孩子节点的序号如何求父节点?

由于计算机除号的取整性,父亲节点 == ( 孩子节点-1 ) / 2

实现搭建堆

根据上面的两个步骤,我们就可以开始搭建堆了

首先是把数组插入到堆中

int main()
{
	HP hp;
	HeapInit(&hp);
	int arr[] = { 9,8,6,5,43,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		HeapPush(&hp, arr[i]);
	}

	return 0;
}

这里假设直接插入,没有进行任何算法调位,那么结果应该是这样的
在这里插入图片描述
如果用向上调整算法进行调整,后面的结果是这样的

在这里插入图片描述

void Swap(HPDataType* child, HPDataType* parent)
{
	HPDataType tmp;
	tmp = *child;
	*child = *parent;
	*parent = tmp;
}

void AdjustUp(HP* php, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (php->a[child] < php->a[parent])
		{
			Swap(&php->a[child], &php->a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		php->a = (HPDataType*)malloc(sizeof(HPDataType) * newcapacity);
		if (php->a == NULL)
		{
			perror("malloc fail");
			return;
		}
	}

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

	AdjustUp(php, php->size - 1);
}

从中可以看出,这样的算法是可以进行堆正确排序的,这样,堆就搭建起来了

下面我们进行堆相关的其他操作

实现出堆的操作

有数据入堆就少不了出堆,那么数据是如何出堆的?

首先要明确是谁出堆,初学可能会认为是堆的最后一个元素,事实上,这样的操作是没有意义的,因此这里出堆,出的是堆顶的元素,那么问题就来了,如何实现出堆的功能?

如果你不加思考去想,这个功能很简单,直接把数组后面的内容覆盖不就好了吗?事实上,这样的想法是错误的,原因在于覆盖后的堆还能维持原来的现状吗?原来的父子关系会变成兄弟关系,原来的兄弟关系也会因为少了一个元素而发生改变,这整个过程会有很大变化,因此,这里引入了第二个算法,向下调整算法

这个算法设计也是很巧妙,假设我们现在搭建的是小堆,那么堆顶的元素是最小元素,现在我们让堆顶这个最小元素和整个堆的最后一个元素互换位置,那么此时堆顶元素变成另外一个元素,但是堆的其他部分依旧符合小堆的规则(交换的原来的最小值堆顶就不计入堆中了,已经被pop掉了),那么接着就可以采用向下调整算法,让这个新的堆顶元素向下调整,这样就能实现目标

下面的图可以很好的解释这个原理

在这里插入图片描述
那么现在就要搞清楚什么是向下调整算法

向下调整算法

首先声明这个算法的使用条件,该算法适用于除了堆顶外的其他部分都满足小堆或大堆的条件时,可以使用,简单来说就是pop堆顶的时候可以使用

使用的原理也相当简单,假设我们这里是小堆,那么堆顶元素被弹出,此时堆中第二小的元素一定是这个堆顶元素的儿子,那么我们就让堆的最后一个叶子来充当这个新的堆顶,这样可以在保持堆整体的构造不变的前提下还能把堆顶元素弹出,紧接着就让这个堆顶元素和下面的儿子进行比较,谁小谁就是新的堆顶,进行交换后第二小的元素就产生了,当然,如果树的高度很高,那么交换后可能需要继续交换,知道这个叶子回到最后一层,这个过程也是可以借助循环实现的,借助这个向下调整算法就可以把堆顶元素弹出的同时还能变成一个新堆,不断的找出最小的值或最大的值

那么下面我们来实现这个算法

void AdjustDown(HP* php, int n, int parent)
{
	assert(php);
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && php->a[child + 1] < php->a[child])
		{
			child++;
		}
		if (php->a[child] < php->a[parent])
		{
			Swap(&php->a[child], &php->a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapPop(HP* php)
{
	assert(php);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	AdjustDown(php, php->size, 0);
}

堆排序

下面说明堆的另外一个作用,可以用来堆排序

首先说明堆排序的原理:假设现在这里有10个数字,现在把这10个数字建成小堆,那么堆顶的元素就是这10个数字的最小值,再让该数字和最后一个元素呼唤位置,这样最小值就到了最后一个位置,再进行向下调整算法可以调整出第二小的元素,按照上方的流程重新来一次,就能弄出新的数字,这样下去就可以实现降序排列的功能

具体操作流程如下

void HeapSort(HPDataType* a, int size)
{
	assert(a);

	//建堆
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, size, i);
	}
	
	//排序
	int end = size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

这样的排序也是有效的

在这里插入图片描述

那么堆排序好在哪里?从时间复杂度来看,堆排序的时间复杂度只有O(NlogN),整体来看效率还是可以的

TopK

堆真正强大的功能其实是强大在从很大一个量级的数字中要找出其中最大或最小的10个,假设这个数字是一亿甚至十亿,那么如果我们还采用的是正常的排序来看,那么整个过程就会相当麻烦,把这些数字全部排序再找最大或最小的几个,这个过程消耗的时间和空间复杂度是不可计算的,甚至计算机没有足够的内存供你建立如此庞大的空间

因此堆可以很好的解决这个问题,堆的功能主要体现在它可以筛选出你要的数据,下面介绍topk的原理

假设我们现在有10000个数字,我们要从中找到最大的5个,那么如何用堆来进行实现?
首先,我们把前五个数字建一个堆,假设我们要找的是最大的五个数,那么我们就建立小堆,然后让后面的数字依次从堆顶看能不能进入堆中,假设这个数字大于堆顶元素,那么就让这个元素称为堆顶元素,再进行向下调整,接着让下一个元素和堆顶比较…

按这样的想法实施下来,就可以使得堆中的元素是所有数字里面最大的五个元素,这样就能实现目标

下面就来模拟实现这个过程

首先,我们要获取到这10000个数据,下面展示获取数据量的一种途径

void CreateData()
{
	int n = 10000;
	srand(time(0));
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen fail");
		return;
	}
	for (int i = 0; i < n; i++)
	{
		int x = rand() % 10000;
		fprintf(pf, "%d\n", x);
	}
	fclose(pf);
}

获取了信息下面开始实现topk的功能

void PrintTopK()
{
	Heap hp = { 0,0,0 };
	HeapCreate(&hp,hp.a,4);
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen fail");
		return;
	}
	int* kmaxheap = (int*)malloc(sizeof(int) * 5);
	if (kmaxheap == NULL)
	{
		perror("malloc fail");
		return;
	}
	for (int i = 0; i < 5; i++)
	{
		fscanf(pf, "%d", &kmaxheap[i]);
		HeapPush(&hp, kmaxheap[i]);
	}
	int val = 0;
	while (!feof(pf))
	{
		fscanf(pf, "%d", &val);
		if (val > kmaxheap[0])
		{
			kmaxheap[0] = val;
			AdjustDown(kmaxheap, 5, 0);
		}
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", kmaxheap[i]);
	}
}

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

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

相关文章

ylb_学习笔记02

1.随机4位数&#xff1a; String random RandomStringUtils.randomNumeric(4);System.out.println("注册验证码的随机数 random"random);2.使用http时判断响应的状态为ture&#xff08;200&#xff09;&#xff1a; response.getStatusLine().getStatusCode() Htt…

在阿里云linux上安装MySql数据库

我们先远程连接服务器 然后输入 sudo yum update重新运行一下 然后 sudo yum install mysql-server安装 mysql 服务 其中有两次 y n 选择 都选y就好了 然后 运行 sudo service mysqld start启动MySql 然后 我们查看一下MySql sudo service mysqld status

谷歌浏览器,网站多账号登陆的方法

在测试系统某些功能的时候&#xff0c;不同的模块&#xff0c;需要不同的权限&#xff0c;所以需要登陆不同的账号&#xff0c;以下有两种办法&#xff0c;可以快速切换账号方便进行测试。 1&#xff0c;使用SessionBox插件 SessionBox是一款可以方便地切换网站的session&…

Android开发笔记

一、知识点 1、Notification 通知的创建流程 1&#xff09;创建一个NotificationManager&#xff0c;获取系统服务&#xff0c;getSystemService()方法&#xff1b; 2&#xff09;使用Builder构造器来创建Notification对象&#xff0c;设置通知的各种属性&#xff1b; 3&#…

postgresql还原bak

1、第一步肯定是要新建自己还原的目标数据库&#xff0c;例如&#xff1a; 2、进入postgresql的安装目录下的bin目录下 然后地址栏输入cmd进入命令 输入以下 psql -h localhost -U postgres -p 5432 -d SamsinoYardStandard_karamay -f "D:\desktop\zk\20230628.bak&quo…

CEO对今天的CIO们真正的要求是什么?

在当今数字化和信息时代&#xff0c;企业的首席信息官&#xff08;CIO&#xff09;的角色变得至关重要。CIO不仅需要具备深厚的技术知识&#xff0c;还需要在商业战略、创新和领导力方面展现出卓越的能力。作为企业的首席执行官&#xff08;CEO&#xff09;&#xff0c;他们对C…

【Distributed】分布式Ceph存储系统

文章目录 一、存储基础1. 单机存储设备1.1 DAS1.2 NAS1.3 SAN1.4 单机存储的问题1.5 商业存储解决方案 2. 分布式存储&#xff08;软件定义的存储 SDS &#xff09;分布式存储的类型 3. Ceph 简介4. Ceph 优势5. Ceph 架构6. Ceph 核心组件7. OSD 存储后端7.1 Filestore7.2 Blu…

单轴机器人的结构与特点

单轴机器人是由马达驱动的移动平台&#xff0c;由滚珠螺杆和 U型线性滑轨导引构成&#xff0c;其滑座同时为滚珠螺杆的驱动螺帽及线性滑轨的导引滑块&#xff0c;可用半导体、光电、交通运输业、环保节能产业、精密工具机、机械产业、智慧自动化、生技医疗上。 相对于传统的模组…

Yalmip工具箱使用教程(1)-入门学习

博客中所有内容均来源于自己学习过程中积累的经验以及对yalmip官方文档的翻译&#xff1a;YALMIP 1.Yalmip工具箱的下载与安装 1.1下载 Yalmip的作者是Johan Lfberg&#xff0c;是由Matlab平台编程实现的一个免费开源数学优化工具箱&#xff0c;在官网上就可以下载。官方下载…

LeetCode 0931. 下降路径最小和:通俗思路讲解

【LetMeFly】931.下降路径最小和&#xff1a;通俗思路讲解 力扣题目链接&#xff1a;https://leetcode.cn/problems/minimum-falling-path-sum/ 给你一个 n x n 的 方形 整数数组 matrix &#xff0c;请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径 可以从第一…

LeetCode 142.环形链表II

142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/ class Solution { public:ListNode *detectCycle(ListNode …

Java前端开发工程师的职责

Java前端开发工程师的职责1 职责&#xff1a; 1.负责公司现有软件的二次使用开发、测试; 2.负责公司信息化管理软件的开发; 3.修改已有的系统方案&#xff0c;以维持优良的操作性能及正常的信息沟通; 4.完善公司系统&#xff0c;完成项目接口、开发工作; 5. 能单独根据需求…

【yarn】 ‘husky install‘ fails if ‘.git‘ directory does not exists解决方法

问题描述 环境&#xff1a;win10 yarn 1.22.19 问题&#xff1a;在使用yarn安装前端依赖时&#xff0c;yarn install 出现错误: .git can’t be found (see https://git.io/Jc3F9) error Command failed with exit code 1. 截图 原因分析 根据设计&#xff0c;husky安装必…

数据结构-双向带头循环链表

链表的分类实现带有哨兵位的双向的循环链表**定义节点的结构**初始化单个节点初始化带有哨兵位的双向循环链表打印链表销毁链表尾插尾删头插头删find函数在任意位置之前插入任意位置的删除全部代码list.hlist.ctest.c 链表和顺序表的区别 链表的分类 如下 根据上述的三种组合…

线性调频信号公式推导及matlab仿真

线性调频信号的数学表达式&#xff1a; 其中&#xff0c;t是时间变量&#xff0c;单位为秒&#xff08;s&#xff09;&#xff1b;T为脉冲持续时间&#xff08;周期&#xff09;&#xff1b;K是线性调频率&#xff0c;单位是Hz/s&#xff1b; 角度&#xff08;单位为弧度&…

【通过粒子滤波进行地形辅助导航】用于地形辅助导航的粒子滤波器和 PCRB研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【MySQL异常解决】MySQL执行SQL文件出现【Unknown collation ‘utf8mb4_0900_ai_ci‘】的解决方案

MySQL执行SQL文件出现【Unknown collation ‘utf8mb4_0900_ai_ci‘】的解决方案 一、背景描述二、报错原因三、解决方案3.1 升级 MySQL 数据库版本3.2 修改字符集为 一、背景描述 从服务器MySQL中导出数据为SQL执行脚本后&#xff0c;在本地电脑执行导出的SQL脚本&#xff0c;…

【数据结构---排序】庖丁解牛式剖析常见的排序算法

排序算法 一、常见的排序算法二、常见排序算法的实现1. 直接插入排序2. 希尔排序3. 直接选择排序4. 堆排序5. 冒泡排序6. 快速排序6.1 递归实现快速排序思路一、hoare 版本思路二、挖坑法思路三、前后指针法 6.2 非递归实现快速排序 7. 归并排序7.1 归并排序的递归实现7.2 归并…

Word2Vec实现文本识别分类

深度学习训练营之使用Word2Vec实现文本识别分类 原文链接环境介绍前言前置工作设置GPU数据查看构建数据迭代器 Word2Vec的调用生成数据批次和迭代器模型训练初始化拆分数据集并进行训练 预测 原文链接 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&…

Apache Phoenix(2):安装Phoenix

1 下载 大家可以从官网上下载与HBase版本对应的Phoenix版本。 http://phoenix.apache.org/download.html 2 安装 &#xff08;1&#xff09;上传安装包到Linux系统&#xff0c;并解压 cd /opt/ tar -xvzf phoenix-hbase-2.5-5.1.3-bin.tar.gz &#xff08;2&#xff09;将p…