【数据结构】堆的实现及TOP-K问题

news2024/11/17 22:47:34

文章目录

  • 前言
  • 1. 堆的概念及结构
  • 2. 堆的实现
    • 2.1 堆向上调整算法
    • 2.2 堆向下调整算法
    • 2.3 堆的创建
    • 2.4 堆的删除
    • 2.5 堆的常用接口代码实现
  • 3. 堆的应用
    • TOP-K问题



前言


在正式讲堆之前,我们要先来讲一下二叉树的顺序结构:

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


1. 堆的概念及结构

如果有一个关键码的集合K = { k k k0 k k k1 k k k2,… , k k kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: K K Ki <= K K K2*i+1 K K Ki <= K K K2*i+2 K K Ki >= K K K2*i+1 K K Ki >= K K K2*i+2) i = 0,1,2,… ,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。堆的性质:

堆的性质:

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

在这里插入图片描述

来看几道题:

1.下列关键字序列为堆的是:()
A 100, 60, 70, 50, 32, 65
B 60, 70, 65, 50, 32, 100
C 65, 100, 70, 32, 50, 60
D 70, 65, 100, 32, 50, 60
E 32, 50, 100, 70, 65, 60
F 50, 100, 70, 65, 60, 32
2.已知小根堆为8, 15, 10, 21, 34, 16, 12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是()。
A 1
B 2
C 3
D 4
3.一组记录排序码为(5 11 7 2 3 17), 则利用堆排序方法建立的初始堆为
A(11 5 7 2 3 17)
B(11 5 7 2 17 3)
C(17 11 7 2 3 5)
D(17 11 7 5 3 2)
E(17 7 11 3 5 2)
F(17 7 11 3 2 5)
4.最小堆[0, 3, 2, 5, 7, 4, 6, 8], 在删除堆顶元素0之后,其结果是()
A[3257468]
B[2357468]
C[2345786]
D[2345678]

// 答案
1.A
2.C
3.C
4.C


2. 堆的实现


在讲堆的实现之前,我们要先来理解一下,堆是一个很讲究的数据结构,设计得是非常美妙的:
在这里插入图片描述

2.1 堆向上调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从最后一个孩子节点开始的向上调整算法可以把它调整成一个大堆。
在这里插入图片描述
讲了这些,我们用代码来实现一下这个过程:

// 向上调整
// 这里我用了一个函数指针,目的是方便我们自己可以选择建大堆还是建小堆,如果不理解这个代码也没事,
// 我们在建大堆或者小堆的时候就需要手动去调整代码
void AdjustUp(HPDataType* a, int child, int (*compare)(const void*, const void*)) 
{
	assert(a);
	int parents = (child - 1) / 2; //找到孩子节点的父节点
	while (child > 0)
	{
		if (compare(&a[parents], &a[child]) > 0)  // 通过用户自己写的一个函数来判断是建大堆还是小堆
		{
			Swap(&a[child], &a[parents]);
			// 调整孩子节点和父节点的位置
			child = parents;
			parents = (child - 1) / 2;
		}
		else 
			break;
	}
}

这就是我们向上调整算法的实现,但相比较于向下调整算法,向上调整的效率还是比较低的,所以我们重点掌握向下调整算法就行了。

2.2 堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
在这里插入图片描述

我们用代码来实现一下这个逻辑:

// 向下调整
// 这里我用了一个函数指针,目的是方便我们自己可以选择建大堆还是建小堆,如果不理解这个代码也没事,
// 我们在建大堆或者小堆的时候就需要手动去调整代码
void  AdjustDown(HPDataType* a, int parents, int size, int (*compare)(const void*, const void*))
{
	assert(a);
	int child = parents * 2 + 1; // 先将左孩子设置为孩子节点
	while (child < size)
	{
		// 这里我们以建大堆为例
		if (child + 1 < size && compare(&a[child], &a[child + 1]) > 0) 
		{
			// 比较左孩子和右孩子,如果右孩子比左孩子大,就将孩子节点++
			child++;
		}
		if (compare(&a[parents], &a[child]) > 0) // 如果父亲节点小于孩子节点,就进行交换
		{
			Swap(&a[parents], &a[child]);
			// 调整孩子节点和父节点的位置
			parents = child;
			child = parents * 2 + 1;
		}
		else
			break;
	}
}

2.3 堆的创建

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

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

在这里插入图片描述

2.4 堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
在这里插入图片描述

2.5 堆的常用接口代码实现

// 交换
void Swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

// 向上调整
// 这里我用了一个函数指针,目的是方便我们自己可以选择建大堆还是建小堆,如果不理解这个代码也没事,
// 我们在建大堆或者小堆的时候就需要手动去调整代码
void AdjustUp(HPDataType* a, int child, int (*compare)(const void*, const void*)) 
{
	assert(a);
	int parents = (child - 1) / 2; //找到孩子节点的父节点
	while (child > 0)
	{
		if (compare(&a[parents], &a[child]) > 0)  // 通过用户自己写的一个函数来判断是建大堆还是小堆
		{
			Swap(&a[child], &a[parents]);
			// 调整孩子节点和父节点的位置
			child = parents;
			parents = (child - 1) / 2;
		}
		else 
			break;
	}
}

// 向下调整
// 这里我用了一个函数指针,目的是方便我们自己可以选择建大堆还是建小堆,如果不理解这个代码也没事,
// 我们在建大堆或者小堆的时候就需要手动去调整代码
void  AdjustDown(HPDataType* a, int parents, int size, int (*compare)(const void*, const void*))
{
	assert(a);
	int child = parents * 2 + 1; // 先将左孩子设置为孩子节点
	while (child < size)
	{
		// 这里我们以建大堆为例
		if (child + 1 < size && compare(&a[child], &a[child + 1]) > 0) 
		{
			// 比较左孩子和右孩子,如果右孩子比左孩子大,就将孩子节点++
			child++;
		}
		if (compare(&a[parents], &a[child]) > 0) // 如果父亲节点小于孩子节点,就进行交换
		{
			Swap(&a[parents], &a[child]);
			// 调整孩子节点和父节点的位置
			parents = child;
			child = parents * 2 + 1;
		}
		else
			break;
	}
}



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

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

// 堆的打印
void HeapPrint(Heap* hp)
{
	assert(hp);
	int i = 0;
	for (i = 0;i < hp->_size;i++)
	{
		printf("%d ", hp->_a[i]);
	}
}


// 堆的插入
void HeapPush(Heap* hp, HPDataType x, int (*compare)(const void*, const void*))
{
	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");
			exit(-1);
		}
		hp->_a = tmp;
		hp->_capacity = newcapacity;
	}

	// 插入
	hp->_a[hp->_size] = x;
	// 向上调整
	AdjustUp(hp->_a, hp->_size, compare);

	hp->_size++;
}

// 堆的删除
void HeapPop(Heap* hp, int (*compare)(const void*, const void*))
{
	assert(hp);
	assert(hp->_a);
	assert(hp->_size >= 0);

	Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
	hp->_size--;
	// 向下调整
	AdjustDown(hp->_a, 0, hp->_size, compare);

}

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

// 堆的数据个数
int HeapSize(Heap* hp)
{
	return hp->_size;
}

// 堆的判空
int HeapEmpty(Heap* hp)
{
	return hp->_size == 0;
}


3. 堆的应用


TOP-K问题

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

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

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

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

    • 前K个最大的元素,则建小堆
    • 前K个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

我们先写一个程序来创建一亿个数的程序,然后我在创建出来的文件中,随机修改了十个数,把他们改得都很大,因为我也不知道我是在哪些地方改的所以就不展示了:

void CreateNDate()
{
	int num = 100000000;
	int i = 0;
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		exit(-1);
	}
	for (i = 0;i < num;i++)
	{
		fprintf(pf, "%d\n", rand() % 100000000);
	}


	fclose(pf);
}

int main()
{
	//创建一个含有一亿个数字的文件
	srand((unsigned int)time(NULL));
	CreateNDate();

	return 0;
}

在这里插入图片描述

接下来我们就来试试吧:

//建堆时,根据该函数来决定大堆还是小堆
int CreakSmallHeap(const void* x, const void* y)
{
	return *(int*)x - *(int*)y;
}

void PrintTopK(int k)
{
	// 定义一个堆结构来存放数据
	Heap hp;
	// 堆的大小取决于我们要找的前k个数
	hp._a = (HPDataType*)malloc(sizeof(HPDataType) * k);
	if (hp._a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp._capacity = k;
	hp._size = 0;

	//打开文件
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}

	//用前k个数建堆
	int i = 0;
	for (i = 0;i < k;i++)
	{
		int a = 0;
		fscanf(pf, "%d", &a);
		HeapPush(&hp, a, CreakSmallHeap);
	}

	//遍历文件,比堆头大的数就进堆,并向下调整
	while (fscanf(pf, "%d", &i) != EOF)
	{
		if (i > hp._a[0])
		{
			hp._a[0] = i;
			AdjusutDown(hp._a, 0, k, CreakSmallHeap);
		}
	}

	for (i = 0;i < k;i++)
	{
		printf("%d ", hp._a[i]);
	}
	printf("\n");
	//销毁栈
	HeapDestory(&hp);
	//关闭文件
	fclose(pf);
}
int main()
{
	创建一个含有一亿个数字的文件
	//srand((unsigned int)time(NULL));
	//CreateNDate();

	PrintTopK(10);

	return 0;
}

结果如下:

在这里插入图片描述
还有一个问题:
可能有人不知道为什么我们找最大的前几个数为什么要建小堆,难道不能建大堆,然后将后面取到的数与堆的最后一个数比较吗?原因是:
在这里插入图片描述

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

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

相关文章

Python 实现给 pdf 文件自动识别标题并增添大纲

一、背景&#xff1a; 客户方提供过来一个开放平台的pdf文档&#xff0c;文档里有几十个接口&#xff0c;没有大纲和目录可以定位到具体内容&#xff0c;了解整体的API功能&#xff0c;观看体验极度差劲&#xff0c;所以想使用Python代码自动解析pdf文档&#xff0c;给文档增添…

AntDB设计之CheckPoint——引言与功能简述

1.引言 数据库服务能力提升是一项系统性的工程&#xff0c;在不同的应用场景下&#xff0c;用户对于数据库各项能力的关注点也不同&#xff0c;如&#xff1a;读写延迟、吞吐量、扩展性、可靠性、可用性等等。国内不少数据库系统通过系统架构优化、硬件设备升级等方式&#xf…

【单片机 TB作品】节拍器,电子音乐节拍器,51单片机,Proteus仿真

节拍器的使用可以使练琴者正确掌握乐曲的速度,从而使音 乐练习达到事半功倍的效果。本课题基于单片机设计具有声光晋 示的电子乐器节拍器,充分利用单片机的定时和中断系统,通过 C语言程序设计,控制外部相关硬件电路,实现对音乐速,度 40~120次/分钟范围内连续可调,节拍114、 2/4…

Redis命令---Hash(哈希)篇 (超全)

目录 1.Redis Hmset 命令 - 同时将多个 field-value (域-值)对设置到哈希表 key 中。简介语法可用版本: > 2.0.0返回值: 如果命令执行成功&#xff0c;返回 OK 。 示例 2.Redis Hmget 命令 - 获取所有给定字段的值简介语法可用版本: > 2.0.0返回值: 一个包含多个给定字段…

Simple Facebook Sign-In

简单的Facebook登录为Android、iOS、Windows、Mac、通用Windows平台(UWP)和Unity制作的WebGL应用程序提供了基于OAuth 2.0的Facebook登录。 优点: ● 跨平台游戏和应用程序的跨平台用户身份验证 ● 无插件,无第三方库,无依赖● 对建筑规模没有影响 ● 客户端-服务器应…

PMP证书可以挂靠吗?

PMP证书不是国内的证书&#xff0c;挂靠不了呀&#xff0c;想挂靠&#xff0c;可以考软考/一建等&#xff0c;里面也有项目管理相关的证书。 PMP证书虽然不能挂靠&#xff0c;但是用处还是很大的&#xff0c;例如提升个人能力、薪资待遇&#xff0c;还有持证可享一些城市的福利…

kafka容灾演练的方案

背景 kafka可以通过MirrorMaker工具把集群的数据从一个集群同步到另一个集群&#xff0c;通过在另一个数据中心创建灾备集群的方式可以做到容灾的效果,但是如果我们不通过如此重量级的工具也想达到容灾演练的目的&#xff0c;可以怎么做呢 kafka简单容灾实现 当原kafka集群发…

计算机网络--作业

作业一 1、比较电路交换、报文交换和分组报文交换优缺点 电路交换 电路交换是以电路连接为目的的交换方式&#xff0c;通信之前要在通信双方之间建立一条被双方独占的物理通道&#xff08;由通信双方之间的交换设备和链路逐段连接而成&#xff09;。 优点&#xff1a; ①由于…

MyBatis学习一:快速入门

前言 公司要求没办法&#xff0c;前端也要了解一下后端知识&#xff0c;这里记录一下自己的学习 学习教程&#xff1a;黑马mybatis教程全套视频教程&#xff0c;2天Mybatis框架从入门到精通 文档&#xff1a; https://mybatis.net.cn/index.html MyBatis 快速入门&#xf…

HackTheBox - Medium - Linux - Bagel

Bagel 今天我开始了《Red Team Development and Operations A Practical Guide》的学习&#xff0c;保持学习&#xff0c;后面差不多到时机后就学CRTOⅡ Bagel 是一款中等难度的 Linux 机器&#xff0c;其特点是电子商店容易受到路径遍历攻击&#xff0c;通过该攻击可以获取应…

Tinker 环境下数据表的用法

如果我们要自己手动创建一个模型文件&#xff0c;最简单的方式是通过 make:model 来创建。 php artisan make:model Article 删除模型文件 rm app/Models/Article.php 创建模型的同时顺便创建数据库迁移 php artisan make:model Article -m Eloquent 表命名约定 在该文件中&am…

k8s中实现pod自动扩缩容

一、k8s应用自动扩缩容概述 1&#xff09;背景&#xff1a; 在实际的业务场景中&#xff0c;我们经常会遇到某个服务需要扩容的场景&#xff08;例如&#xff1a;测试对服务压测、电商平台秒杀、大促活动、或由于资源紧张、工作负载降低等都需要对服务实例数进行扩缩容操作&…

SD-WAN组网方式详解

企业网络的演进势不可挡&#xff0c;对于高效、可靠的网络连接需求日益增加。SD-WAN&#xff08;软件定义广域网&#xff09;作为一项创新的网络技术&#xff0c;备受企业青睐并得到广泛应用。SD-WAN提供了多种灵活的组网方式&#xff0c;以满足企业多样化的需求和不同的网络环…

AI实景无人直播创业项目:开启自动直播新时代,一部手机即可实现财富增长

在当今社会&#xff0c;直播已经成为了人们日常生活中不可或缺的一部分。无论是商家推广产品、明星互动粉丝还是普通人分享生活&#xff0c;直播已经渗透到了各行各业。然而&#xff0c;传统直播方式存在着一些不足之处&#xff0c;如需现场主持人操作、高昂的费用等。近年来&a…

亚信安慧AntDB数据库引领数字时代:数字驱动创新峰会主旨演讲深度解析

近日&#xff0c;庄严肃穆的数字驱动创新峰会在中国首都北京隆重召开&#xff0c;聚焦于探讨数据经济的创新前沿。在此次盛会中&#xff0c;备受瞩目的亚信安慧AntDB数据库荣幸受邀参与&#xff0c;该数据库的副总裁张桦以其深刻见解和卓越经验发表了引人瞩目的主旨演讲。 图1&…

立体匹配算法(Stereo correspondence)

SGM(Semi-Global Matching)原理&#xff1a; SGM的原理在wiki百科和matlab官网上有比较详细的解释&#xff1a; wiki matlab 如果想完全了解原理还是建议看原论文 paper&#xff08;我就不看了&#xff0c;懒癌犯了。&#xff09; 优质论文解读和代码实现 一位大神自己用c实现…

系列九、Feign

一、Feign 1.1、Java中如何实现跨接口调用 &#xff08;1&#xff09; Httpclient Httpclient是Apache Jakarta Comon下的子项目&#xff0c;用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包&#xff0c;并且它支持HTTP协议的最新版本和建议。HttpC…

鸿蒙原生应用再添新丁!中国移动 入局鸿蒙

鸿蒙原生应用再添新丁&#xff01;中国移动 入局鸿蒙 来自 HarmonyOS 微博1月2日消息&#xff0c;#中国移动APP启动鸿蒙原生应用开发#&#xff0c;拥有超3亿用户的中国移动APP宣布&#xff0c;正式基于HarmonyOS NEXT启动#鸿蒙原生应用#及元服务开发。#HarmonyOS#系统的分布式…

openGauss学习笔记-183 openGauss 数据库运维-升级-升级操作

文章目录 openGauss学习笔记-183 openGauss 数据库运维-升级-升级操作183.1 就地升级和灰度升级操作步骤 openGauss学习笔记-183 openGauss 数据库运维-升级-升级操作 介绍就地升级、灰度升级和滚动升级的详细操作。 183.1 就地升级和灰度升级操作步骤 以root身份登录节点。 …

突发!博世「裁员」

对于未来几年的汽车行业需求变化&#xff0c;一级零部件供应商正在加快「降本增效」举措&#xff0c;犹如下游客户更加倾向于降本&#xff0c;而不是无休止的提升整车性能&#xff0c;比如&#xff0c;续航里程、智能化。 本周&#xff0c;全球汽车零部件龙头供应商博世宣布&am…