【数据结构】基础:堆

news2025/1/17 21:40:42

【数据结构】基础:堆

摘要:本文主要介绍数据结构堆,分别介绍其概念、实现和应用。


文章目录

  • 【数据结构】基础:堆
    • 一、概述
      • 1.1 概念
      • 1.2 性质
    • 二、实现
      • 2.1 定义
      • 2.2 初始化与销毁
      • 2.3 入堆
      • 2.4 出堆
      • 2.5 堆的创建
      • 2.6 其他
    • 三、应用
      • 3.1 堆排序
      • 3.2 Top-K问题

一、概述

1.1 概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:K[i] <= K[2i+1] 且 K[i]<= K[2i+2] (K[i] >= K[2i+1] 且 K[i] >= K[2i+2]) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。逻辑结构与物理结构图示如下:

1.2 性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值

    小堆:所有父亲都小于或者等于孩子

    大堆:所有父亲都大于或者等于孩子

  • 堆总是一棵完全二叉树

  • 父亲与孩子下标关系

    parent_pos = (child_pos - 1) /2

    rightChild_pos = parent_pos * 2 + 2

    leftChild_pos = parent_pos * 2 + 1

  • 左孩子下标为奇数,右孩子下标为偶数

二、实现

2.1 定义

由于栈的定义过程中,物理结构为数组,因此在此使用顺序表的形式实现,而定义的结构体,与顺序表相似

typedef int HeapDataType;

struct Heap {
	HeapDataType* arr;
	int size;
	int capacity;
};

typedef struct Heap Heap;

2.2 初始化与销毁

初始化:对栈的每个成员进行赋初值

void HeapInit(Heap* pheap) {
	assert(pheap);
	pheap->arr = NULL;
	pheap->capacity = pheap->size = 0;
}

销毁:由于是通过顺序表的形式呈现,因此会在栈区开辟空间,销毁即释放在栈区开辟的空间

void HeapDestory(Heap* pheap) {
    assert(pheap);
	free(pheap->arr);
	pheap->arr = NULL;
	pheap->size = pheap->capacity = 0;
}

2.3 入堆

由于堆的特殊性质,不可以随意对堆进行插入。从逻辑结构入手,可以现在末尾插入,然后不断往上移动,这样就不会影响其他子树,对堆的影响程度较小,而且每次移动进行的是交换操作,高度相较于各个节点来说是很小的,因此复杂度也会小。向上移动的方法是与父节点进行比较,如果子节点较大,就可以与父节点进行交换,并与新的子树继续比较,直到为根或者不需要当前位置合法(比父节点小)为止。当然还需要注意各种细节,有了这样的思路,可以具体书写代码流程与图示如下(以大堆为例):

  1. 断言判断堆是否有效
  2. 判断是否需要扩容
  3. 尾插
  4. 向上移动

void AjustUp(HeapDataType* arr, int child_pos) {
	int parent_pos = (child_pos - 1) / 2;
	while (child_pos > 0) {
		if (arr[parent_pos] < arr[child_pos]) {
			swap(&arr[parent_pos], &arr[child_pos]);
			child_pos = parent_pos;
			parent_pos = (child_pos - 1) / 2;
		}
		else
			break;
	}
}
void HeapPush(Heap* pheap, HeapDataType val) {
	assert(pheap);
	// 判断是否需要扩容
	if (pheap->capacity == pheap->size) {
		int capacityTemp = pheap->capacity == 0 ? 4 : pheap->capacity * 2;
		HeapDataType* ptemp = (HeapDataType*)realloc(pheap->arr,sizeof(HeapDataType) * capacityTemp);
		if (ptemp == NULL) {
			perror("realloc");
			exit(-1);
		}
		pheap->arr = ptemp;
		pheap->capacity = capacityTemp;
	}
	// 插入到最后一个
	pheap->arr[pheap->size] = val;
	pheap->size++;
	// 对最后一个元素上移操作
	AjustUp(pheap->arr, pheap->size - 1);
}

2.4 出堆

出堆实际上是要根进行出堆,如果直接覆盖,会出现许多问题,既不符合堆的性质,也有较高的复杂度。为了减少对堆的影响,首先将堆的根与末尾进行交换,删除末尾,将新的根节点进行下移。这样复杂度与高度相关,复杂度较低。下移的思路为对找出最大子节点,如果父节点比最大子节点小,就与其进行交换,并重新确定父节点与最大子节点,持续循环下去,直到为叶节点。

void AjustDown(HeapDataType* arr, int len, int parent_pos) {
	int child_pos = parent_pos * 2 + 1;
	while (child_pos < len) {
		if (child_pos + 1 < len && arr[child_pos + 1] > arr[child_pos]) {
			child_pos = child_pos + 1;
		}

		if (arr[parent_pos] < arr[child_pos]) {
			swap(&arr[parent_pos], &arr[child_pos]);
			parent_pos = child_pos;
			child_pos = parent_pos * 2 + 1;
		}
		else
			break;
	}
}
void HeapPop(Heap* pheap) {
	assert(pheap);
	assert(!isHeapEmpty(pheap));
	//最后一个与根交换
	swap(&pheap->arr[0], &pheap->arr[pheap->size - 1]);
	pheap->size--;
	//下移
	AjustDown(pheap->arr, pheap->size, 0);
	return;
} 

2.5 堆的创建

堆的创建有两种方法,一种是逐个插入,另外可以逐渐构建堆直到构建整个堆为止。由于第二种方法的复杂度更低,因此在此介绍第二种方法。该方法即找出最小的父节点,然后对父节点进行循环,从小的堆到大的对,构建好每一个堆。图示与代码如下:

void HeapCreate(Heap* pheap, HeapDataType* arr, size_t len) {
	assert(pheap);
	HeapDataType* ptemp = (HeapDataType*)malloc(sizeof(HeapDataType) * len);
	if (ptemp == NULL) {
		perror("malloc");
		exit(-1);
	}
	pheap->arr = ptemp;
	memmove(pheap->arr, arr, sizeof(HeapDataType) * len);
	pheap->size = len;
	pheap->capacity = len;

	int endLeaf = pheap->size - 1;
	int parent = (endLeaf - 1) / 2;
	while (parent >= 0) {
		AjustDown(pheap->arr, pheap->size, parent);
		parent -= 1;
	}
	return;
}

**堆的建立时间复杂度分析:**对于逐个插入数据后自顶向下的维护堆结构与从底向上逐步构建根节点维持堆结构两个算法,虽然看起来时间复杂度都是O(nlgn),实际上二者的时间复杂度是存在差异的,仔细观察可以发现,对于每一棵子树的根节点和叶节点的数目差不多,因此遍历的数量会少很多,在此通过计算来分析二者的时间复杂度。以满二叉树举例,便于计算以及分析,计算过程如下:

2.6 其他

HeapDataType HeapTop(Heap* pheap) {
	return pheap->arr[0];
}
int HeapSize(Heap* pheap) {
	return pheap->size;
}
bool isHeapEmpty(Heap* pheap) {
	return pheap->size == 0;
}

三、应用

3.1 堆排序

堆排序的主要思路是通过堆得性质,不断从堆顶进行取值出堆,并保持堆的性质不变。通过升序排序为例,具体思路为,初始化时通过建堆的思路将需要排序的数组建成一个大堆(每次出堆最大值,将其放在末尾),建立好后,将最大值与末尾交换,进行下移算法,保持堆的性质,重复此过程知道所有元素排序完成。

堆排序的第一个细节是思考建立大堆还是小堆,如果为升序,那就是需要大堆,建立大堆可以将堆顶元素与末尾交换,并将数组长度减一,这样对于数组来说,堆的性质不变的,而如果是小堆则无法达成该目的。

对于堆排序的时间复杂度为:O(lgn)

具体代码如下:

void HeapSorted(HeapDataType arr[], int len) {
	// 升序
	// 变成一个大堆
	// 最小子树的根节点
	int parent = (len - 1) / 2;
	while (parent >= 0) {
		AjustDown(arr, len, parent);
		parent--;
	}
	// 将最后一个与第一个互换,向下调整
	int length = len;
	for (int i = 0; i < len; i++) {
		swap(&arr[0], &arr[length - 1]);
		AjustDown(arr, --length, 0);
	}
	return 0;
}

3.2 Top-K问题

TOP-K即求数据中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。一般情况下,如果使用排序的话复杂度会过高,但使用堆的数据结构可以很好的解决该问题。

对于实现思路主要有两种(以求最大值为例):

  • 建立一个N个数据的大堆,出堆k次,依次取堆顶,这种方法适用于数据在内存的时候,不会有额外的空间消耗,时间复杂度为 O(N+ Nlgk)
  • 建立一个k个数据的小堆,依次遍历数组,比堆顶大的数据就替代堆顶,再向下调整,直到数组遍历完,最后最大的k个数就在小堆中,这种思路直接取部分数据到小堆中,每次出堆最小的,因此最后得到的是前k个大的数,这种方法的时间复杂度为:O(k+(N-k)logk) = O(Nlgk),但时间复杂度为O(K),实际上消耗也很小,但使用与更多长场景。

在此我们对第二种方法对最小值topk问题进行实现,代码示例如下:

void  PrintTopK(HeapDataType arr[], int n, int k) {
	// 最小k个数
	// 建立一个k个数据的大堆
	HeapDataType* arr_temp = (HeapDataType*)malloc(sizeof(HeapDataType) * k);
	if (arr_temp == NULL) {
		perror("arr_temp malloc failed");
		exit(1);
	}
	for (int i = 0; i < k; i++) {
		arr_temp[i] = arr[i];
	}
	Heap* minKHeap = (Heap*)malloc(sizeof(Heap));
	HeapInit(minKHeap);
	// 遍历数组当有更小的数时,出最大堆数,入数组的数
	HeapCreate(minKHeap, arr_temp, k);
	for (int i = 0; i < n; i++) {
		if (HeapTop(minKHeap) > arr[i]) {
			HeapPop(minKHeap);
			HeapPush(minKHeap, arr[i]);
		}
	}
	printf("TopK:");
	for (int i = 0; i < k; i++) {
		printf("%d ", minKHeap->arr[i]);
	}
	printf("\n");
	free(minKHeap);
	free(arr_temp);
	return;
}
void test4() {
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	srand(time(0));
	for (size_t i = 0; i < n; ++i)
	{
		a[i] = rand() % 1000000;
	}
	a[5] = -1;
	a[1231] = -2;
	a[531] = -3;
	a[5121] = -4;
	a[115] = -5;
	a[2335] = -6;
	a[9999] = -7;
	a[76] = -8;
	a[423] = -9;
	a[3144] = -10;
	PrintTopK(a, n, 10);
	free(a);
}

补充:

  1. 代码将会放到:C_C++_REVIEW: 一套 C/C++ 系统完整的使用手册,以及部分疑难杂症的解析 (gitee.com) ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!

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

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

相关文章

《前端》css总结(下)

文章目录元素展示格式displaywhite-spacetext-overflowoverflow内边距和外边距marginpadding盒子模型box-sizing位置position&#xff1a;用于指定一个元素在文档中的定位方式浮动flex布局flex-directionflex-wrapflex-flowjustify-contentalign-itemsalign-contentorderflex-g…

你最少用几行代码实现深拷贝?

问题分析 深拷贝 自然是 相对 浅拷贝 而言的。 我们都知道 引用数据类型 变量存储的是数据的引用&#xff0c;就是一个指向内存空间的指针&#xff0c; 所以如果我们像赋值简单数据类型那样的方式赋值的话&#xff0c;其实只能复制一个指针引用&#xff0c;并没有实现真正的数…

计算机组成原理4小时速成:存储器,内存ROM,RAM,Cache,高速缓存cache,外存,缓存命中率,效率

计算机组成原理4小时速成&#xff1a;存储器&#xff0c;内存ROM,RAM,Cache&#xff0c;高速缓存cache&#xff0c;外存&#xff0c;缓存命中率&#xff0c;效率 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法…

MYSQL事务原理分析

目录事务是什么ACID特性原子性&#xff08;A&#xff09;隔离性&#xff08;I&#xff09;持久性&#xff08;D&#xff09;一致性&#xff08;C&#xff09;隔离级别简介有些什么READ UNCOMMITTED&#xff08;读未提交&#xff09;READ COMMITTED&#xff08;读已提交&#xf…

【17】Java常见的面试题汇总(设计模式)

目录 1. 说一下你熟悉的设计模式&#xff1f; 2. 简单工厂和抽象工厂有什么区别&#xff1f; 1. 说一下你熟悉的设计模式&#xff1f; 单例模式&#xff1a;保证被创建一次&#xff0c;节省系统开销。 工厂模式&#xff08;简单工厂、抽象工厂&#xff09;&#xff1a;解耦…

力扣(LeetCode)60. 排列序列(C++)

枚举 枚举每一位可能的数字。 如图。算法流程如上图。 思路分析 : 一个数 nnn &#xff0c;可以组成的排列数量有 n!n!n! 。当首位确定&#xff0c;剩余位能组成的排列数量 (n−1)!(n-1)!(n−1)! &#xff0c;依次类推 (n−2)!/(n−3)!/(n−4)!/…/2!/1!/0!(n-2)!/(n-3)!/(n…

CentOS7安装MYSQL8.X的教程详解

1-首先查看系统是否存在mysql&#xff0c;无则不返回 1 # rpm -qa|grep mysql 2-安装wget 1 # yum -y install wget 3-抓取mariadb并删除包&#xff0c;无则不返回 1 # rpm -qa|grep mariadb 4-删除mariadb-libs-5.5.68-1.el7.x86_64 1 # rpm -e --nodeps mariadb-libs-…

本节作业之数组求和及其平均值、求数组最大值、最小值、数组转换为分割字符串、新增数组案例、筛选数组案例、删除指定数组元素、翻转数组、数组排序(冒泡排序)

本节作业之数组求和及其平均值、求数组最大值、最小值、数组转换为分割字符串、新增数组案例、筛选数组案例、删除指定数组元素、翻转数组、数组排序<冒泡排序>求数组[2,6,1,7,4]里面所有的元素的和以及平均值求数组[2,6,1,77,52,25,7]中的最大值求数组[2,6,1,77,52,25,7…

Linux - netstat 查看系统端口占用和监听情况

文章目录功能语法示例显示 tcp&#xff0c;udp 的端口和进程Show both listening and non-listening socketsList all tcp ports.List all udp portsList only listening portsList only listening TCP ports.List only listening UDP ports.List only the listening UNIX port…

Android 性能优化方法论【总结篇】

作为一位多年长期做性能优化的开发者&#xff0c;在这篇文章中对性能优化的方法论做一些总结&#xff0c;以供大家借鉴。 性能优化的本质 首先&#xff0c;我先介绍一下性能优化的本质。我对其本质的认知是这样的&#xff1a;性能优化的本质是合理且充分的使用硬件资源&#x…

分享感恩节联系客户话术

看了一眼在手边的日历&#xff0c;发现今天是11月24日&#xff0c;一年一度的感恩节又到了&#xff0c;不得不感叹时间过得真快&#xff0c;每年十一月的第四个星期四是感恩节。 随着各国多元文化的发展&#xff0c;感恩节也越来越被世界各地传颂&#xff0c;多少都会有一些影…

WANLSHOP 直播短视频种草多用户电商系统源码自营+多商户+多终端(H5+小程序+APP)

WANLSHOP高级版 可用于自营外包项目(多主体)、 可用于外包定制开发项目、 提供开源源码&#xff0c;私有化部署、一款基于 FastAdmin Uni-APP 开发的 多终端&#xff08;H5移动端、APP、微信小程序、微信公众号&#xff09;、多用户商城系统拥有多种运营模式B2B2C/B2C&#xf…

python安装依赖库

一、安装pip 1、打开终端&#xff0c;输入&#xff1a; pip3 install tushare -i https://pypi.douban.com/simple 2、验证是否安装成功 打开终端&#xff0c;输入&#xff1a;pip3 出现以上页面&#xff0c;则安装成功 二、安装flask 1、打开终端&#xff0c;输入&…

mybatis-plus学习笔记

文章目录1 简介2 初始化项目2.1引入pom2.2 引入lombok插件2.3 配置信息2.4 创建实体类2.5 创建mapper2.6 配置注解MapperScan2.7 编写测试类2.8 配置MyBatis日志3 测试基本的CRUD3.1 新增3.2 查询3.3 修改3.4 删除4 自定义动态sql5 Service 层使用mybatis-plus方法5.1 service层…

使用hive进行大数据项目分析

目录 一、首先要创建表&#xff1a;在txt记事本中先输入创建语句备用&#xff0c;创建class1~class5的表。 二、启动hadoop集群&#xff0c;MySQL&#xff0c;hive。 三、创建数据库zhh&#xff0c;用户为zhh&#xff0c;之后将之前写在txt记事本里的创建表class1~class5的命…

浅谈企业信息化安全建设中的三大误区

伴随着信息化的深度建设与应用&#xff0c;与之相伴的信息安全事件也层出不穷&#xff01;很多企业开始关注信息安全问题、关注信息安全建设&#xff0c;大家的共识已经达到前所未有的高度。 虽然许多的企业虽然认识到信息安全的重要性&#xff0c;在实际实施过程中却又无从下…

【附源码】计算机毕业设计JAVA亦心化妆品网站

【附源码】计算机毕业设计JAVA亦心化妆品网站 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA myba…

【软件测试】我们测试人搭上元宇宙的列车,测试一直在进军......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 虚拟宇宙&#xff0…

微信抽奖小程序开发_分享微信抽奖小程序制作的步骤

各位商家在节日期间做活动的时候&#xff0c;都希望用更少的费用去或者更好的宣传和推广的效果。比较常见的就是抽奖活动小程序。无须玩家下载&#xff0c;通过微信扫码或者指定入口就可以参与。 方便&#xff0c;效果又好。 那么,性价比高的抽奖活动小程序怎么做&#xff1f…

使用 MITRE ATTCK 技术保护您的 Active Directory安全

Active Directory (AD域)保存着企业的敏感数据&#xff0c;例如用户凭据、员工的个人信息、安全权限等。正因为如此&#xff0c;AD域很容易成为网络攻击者的目标。恶意攻击者不断升级新的攻击策略&#xff0c;使企业保护AD域安全成为一项挑战。这就是为什么每个企业都必须制定网…