【数据结构】堆的实现

news2024/9/27 19:20:18

  • 1.堆:一种二叉树
  • 2.堆的概念及结构
  • 3.堆的实现
    • 3.1 创建堆的结构
    • 3.2 堆的初始化
    • 3.3 堆的插入
    • 3.4 堆的向上调整法(up)
    • 3.5 打印堆的数据
    • 3.6 到这里就可以实现一个基本的堆了
    • 3.7 向下调整法down(非常重要的一个方法)
    • 3.8 最优的建堆算法
      • 3.8.1 建堆算法时间复杂度分析
    • 3.9堆的一些操作(pop,top,size,empty)
      • 3.9.1 堆的删除 pop
      • 3.9.2 取堆顶元素
      • 3.9.2 堆的大小
      • 3.9.3 判断堆是否为空
    • 3.10 堆的销毁
  • 4.总结

1.堆:一种二叉树

是一种完全二叉树,使用顺序结构的数组来存储。
不了解完全二叉树的小伙伴可以看看这篇文章,这里不再赘述
二叉树
使用完全二叉树可以避免空间的浪费,因为完全二叉树结点之间从逻辑图看一定是连续的
在这里插入图片描述
我们要做的是像最左边的逻辑图那样利用顺序结构实现堆

需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

2.堆的概念及结构

在这里插入图片描述

堆的性质:
1.堆中某个结点的值总是不大于或不小于父节点的值
2.堆总是一棵完全二叉树

如图所示
在这里插入图片描述

3.堆的实现

3.1 创建堆的结构

typedef int data_type; //把int typedef这样可以方便以后随时更换数据类型

typedef struct heap // 这里默认是大根堆
{
	int* a;   // 数组模拟堆
	int size; // 元素数量
	int capacity; // 堆的当前容量
}heap; 

3.2 堆的初始化

void init(heap* h)
{
	assert(h); //断言一下,防止传入空
	h->a = NULL;
	h->size = 0;
	h->capacity = 4; // 容量初始化为 4 
	int* tmp = (heap*)malloc(sizeof(heap) * h -> capacity);
	//malloc动态开辟数组大小
	if (tmp == NULL)  //养成好习惯,malloc后判断是否开辟成功
	{
		perror("malloc");
		exit(-1);
	}
	h->a = tmp; // 开辟成功后,再把开辟的空间给我们的堆的数组
}

3.3 堆的插入

我们要往堆里面插入数据,最好的方法就是在数组的末尾插入,因为如果在数组前面插入的话,整个数组的其他元素还要往后挪,在中间插入也要挪,最好的方法就是往末尾插入,再去调整它,保持大根堆或者小根堆,我们这里默认实现大根堆。

void push(heap* h, data_type x) // x 是我们要插入的数据
{
	assert(h);
	if (h -> size == h->capacity) //判断容量是否满了,满了就扩容
	{
		int* tmp = (heap*)realloc(h->a, sizeof(heap) * h->capacity * 2);//每次扩两倍
		if (tmp == NULL) //养成好习惯判断是否扩容成功
		{
			perror("malloc");
			exit(-1);
		}
		h->a = tmp; // 扩容成功再赋给堆的数组a
		h->capacity *= 2; //别忘了我们的结构体成员变量也要 *2
	}
	h->a[h->size] = x; //往size的位置插入x,每次都在末尾插入
	h->size++;//插入后每次size++,方便下次插入
	up(h -> a, h -> size - 1); // 然后使用向上调整法,我们下面讲
	//每插入一个数据就利用向上调整
}

3.4 堆的向上调整法(up)

向上调整法就是找到一个结点,然后拿这个结点与它的父亲去比较,
如果我们想实现大根堆,就判断这个结点是否比它的父亲大,满足条件就和这个的结点的父亲交换。
然后一直向上做判断,交换,做判断,交换。知道走到最顶层为止。
时间复杂度为 O(log2 n)

void up(data_type* a,int child)
{
	int parent = (child - 1) / 2; //利用孩子结点找父亲结点公式
	while (child > 0) // 走到最顶层为止
	{
		if (a[child] > a[parent])//如果孩子结点大于父亲
		{
			swap(&(a[child]), &(a[parent]));//交换
			child = parent; //把父亲结点变换成孩子结点,继续向上做判断,交换
			parent = (child - 1) / 2;//再一次找新孩子的父亲
		}
		else //如果孩子结点小于父亲,就不满足条件,break这次循环
		{
			break;
		}
	}
}

代码里的swap函数

void swap(data_type * n1, data_type* n2)
{
	int tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}

3.5 打印堆的数据

我们先实现堆的打印,然后就可以查看堆里的数据了。

void print(heap* h)
{
	assert(h);
	for (int i = 0; i < h->size; i++)
	{
		printf("%d ", h->a[i]);
	}
	printf("\n");
}

3.6 到这里就可以实现一个基本的堆了

void test1() // 测试
{
	heap h1;
	init(&h1);
	push(&h1, 50); 
	push(&h1, 25); 
	push(&h1, 90);
	push(&h1, 33);
	push(&h1, 12);
	print(&h1); // 结果是 90 33 50 25 12 
    //可以手动按顺序画个逻辑图,会发现确实是大根堆
}

我们模拟一下这个过程

一开始插入50,h1->a[0] = 50,h1 -> size++ ,此时size = 1
然后每插入一个数向上调整
up(h1->a,h1->size - 1 ) size - 1 是因为前面提前加了1
因为一开始只有一个数,所以up函数实际没用。
在这里插入图片描述
插入25后,调用up,发现25 < 50, 我们这里实现的是大根堆,所以不做交换。
接下来插入 90
在这里插入图片描述
调用一次up ,发现这个结点大于我的父结点,交换。
在这里插入图片描述

在这里插入图片描述
剩下的数据同理,大家可以自行模拟感受一下。

3.7 向下调整法down(非常重要的一个方法)

向下调整法就是找到该节点的孩子,判断我的两个孩子谁比我大或者谁比我小,实现大根堆或者小根堆。
比如我们想实现大根堆,我们找到一个结点的两个左右孩子,如果孩子比父亲大,就交换。
注意,这里可以两个孩子都比父结点大,所以我们要找到最大的孩子交换。
然后一直往下判断,交换,判断,交换,达到最底层为止。

已知根结点求孩子的方法

left_child = (parent * 2) + 1 //左孩子
right_child = (parent * 2) + 2// 右孩子

向下调整法代码如下:

void down(data_type* a, int len, int parent)
{
	int child = (parent * 2) + 1; // 先假设左孩子最大
	while (child < len) //直到达到最底层就停止
	{
		//这里加1可能会有越界问题,可能会访问到随机值,导致出错,判断一下
		if (child + 1 < len && a[child + 1] > a[child]) // 如果右孩子大的话就 child++
		{
			child++;
		}
		if (a[parent] < a[child])//如果父结点小于孩子结点就交换
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = (parent * 2) + 1;
		}
		else
		{
			break;//否则break
		}
	}
}

3.8 最优的建堆算法

向下调整法是 建堆算法一个非常重要的算法。
注意这里的建堆不同于上面的插入建堆,而是随便给你一个数组,要求你在最优的时间复杂度,把这个数组建立成堆,也就是大根堆或者小根堆。
如果我们一个一个插入去实现,时间复杂度就是O(n * log2n)
或者利用向上调整法,从第二层开始逐层向上调整,时间复杂度也是O(n* log2n)
而如果利用我们的向下调整法,从倒数第二层开始向下调整,然后倒数第三层向下调整,直到最顶层,是可以做到时间复杂度为O(n)的。
最简单的一个优化理解就是,二叉树中一般最后一层占的结点最多,一般占了总结点的一半以上,我们直接省去了最后一层的计算,所以优化成了O(n)

void creat_heap(heap* h, data_type *arr, int n)  //建堆算法:从倒数第二层开始向下调整
{//这里的建堆是指传入一个数组,把它建成大堆或者小堆
	//时间复杂度O(n)
	assert(h);
	//malloc一个堆的新数组
	h->a = (heap*)malloc(sizeof(heap) * n);
	if (h -> a == NULL)
	{
		perror("malloc:");
		exit(-1);
	}
	//把原来数组的内容复制到新数组
	memcpy(h -> a, arr, sizeof(data_type) * n);
	h->size = h->capacity = n;//大小和容量也给过去
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
	// (n - 1 - 1) / 2 求倒数第一个非叶结点的位置
	//从倒数的第一个非叶子节点的子树开始调整
		down(h -> a, n, i);
	}
}

3.8.1 建堆算法时间复杂度分析

void test2()
{
	heap h2;
	int arr[] = { 1,5,3,8,7,6 };
	int n = sizeof(arr) / sizeof(arr[0]);
	creat_heap(&h2, arr,n);
	print(&h2); // 8 7 6 5 1 3
}

在这里插入图片描述
在这里插入图片描述

其实最简单的理解方式还是上面说的,最后一层的结点几乎占了总结点的一半,我们的向下调整法从倒数第一个非叶子开始,就省去了最后一层结点的计算,所以才能得到优化。

3.9堆的一些操作(pop,top,size,empty)

3.9.1 堆的删除 pop

堆的删除指的是删除堆顶的元素。

如果是直接删除堆顶元素,那我这个数组还要往前挪动,再向下调整
时间复杂度就是
O(n + log2n)
这种方法不怎么好。

我们这里的删除操作是用堆顶元素与堆最后的元素交换,然后size–
交换后,堆顶元素再向下调整,重新调整堆。
时间复杂度为 O(log2n)

void pop(heap* h)
{
	assert(h);
	assert(h->size > 0);
	swap(&h -> a[0], &h -> a[h -> size - 1]);
	h->size--;
	down(h->a, h->size, 0);
}

3.9.2 取堆顶元素

data_type top(heap* h)
{
	assert(h);
	return h->a[0];
}

3.9.2 堆的大小

int heap_size(heap* h)
{
	assert(h);
	return h->size;
}

3.9.3 判断堆是否为空

bool heap_empty(heap* h)
{
	assert(h);
	return h->size == 0;
}

3.10 堆的销毁

void destroy(heap* h)
{
	assert(h);
	free(h->a);
	h->a = NULL;
	h->size = h->capacity = 0;
}

4.总结

以上就是堆的实现
重点是建堆算法
在后续文章中我们会讲到堆排序,和利用堆解决topK问题(前k大或前k小的数)

其实堆在C++中已经实现好了,我们直接用就好了,但是理解底层的实现原理,才能让我们在编程这条路上走的更远。

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

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

相关文章

Java项目:JSP校园运动会管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 本项目包含三种角色&#xff1a;运动员、裁判员、管理员&#xff1b; 运动员角色包含以下功能&#xff1a; 运动员登录,个人信息修改,运动成绩…

【网络安全】——sql注入之奇淫巧技bypass(持续更新中)

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

微信点餐小程序开发_分享微信点餐小程序可以实现哪些功能

线下餐饮实体店都开始摸索发展网上订餐服务。最多人选择的是入驻外卖平台&#xff0c;但抽成高&#xff0c;推广还要另买流量等问题&#xff0c;也让不少商家入不敷出。在这种情况下&#xff0c;建立自己的微信订餐小程序&#xff0c;做自己的私域流量是另一种捷径。那么&#…

Redis关闭持久化

版本&#xff1a; 7.0.4 一、持久化说明 1、redis默认是开启持久化的 2、默认持久化方式为RDB 二、redis 关闭持久化 关闭 RDB 持久化 1、注释掉原来的持久化规则 # save 3600 1 300 100 60 10000或# save 3600 1 # save 300 100 # save 60 100002、把 save 节点设置为空 s…

GORM CRUD 5 分钟快速上手

文章目录1.ORM 是什么2.GORM 是什么3.安装4.连接 DB5.创建数据表6.增加&#xff08;Create&#xff09;7.查询&#xff08;Read&#xff09;8.更新&#xff08;Update&#xff09;9.删除&#xff08;Delete&#xff09;10.小结参考文献1.ORM 是什么 ORM&#xff08;Object Rel…

Linux文件压缩和解压命令【gzip、gunzip、zip、unzip、tar】【详细总结】

解压和压缩gzip/gunzipgzip 压缩文件gunzip 解压缩文件zip/unzipzip命令语法命令选项实例unzip语法&#xff1a;命令选项实例tar语法实例例一&#xff1a;将文件打包成tar包例二&#xff1a;查阅 tar包内有哪些文件例三&#xff1a;将tar 包解压gzip/gunzip gzip用于解压文件&…

纵目科技冲刺科创板上市:拟募资20亿元,股东阵容强大

11月23日&#xff0c;纵目科技&#xff08;上海&#xff09;股份有限公司&#xff08;下称“纵目科技”&#xff09;在上海证券交易所递交招股书&#xff0c;准备在科创板上市。本次冲刺上市&#xff0c;纵目科技计划募资20亿元&#xff0c;拟用于上海研发中心建设项目、东阳智…

Redis常用指令汇总

文章目录一、5种数据类型二、常用指令汇总三、应用汇总提示&#xff1a;以下是本篇文章正文内容&#xff0c;Redis系列学习将会持续更新 一、5种数据类型 Redis 数据存储格式&#xff1a;  ● redis 自身是一个 Map ,其中所有的数据都是采用 key : value 的形式存储。  ● 数…

【c++】 继承的相关问题

继承无论是那种继承方式&#xff0c;基类继承的私有属性都无法访问不论父类中的属性被啥修饰&#xff0c;都会被子类全盘接收public,protected,private继承private 继承和protected 继承都是类中可以访问&#xff0c;类外无法访问&#xff0c;这有什么区别呐&#xff1f;继承的…

区间信息维护与查询【最近公共祖先LCA 】 - 原理

区间信息维护与查询【最近公共祖先LCA 】 - 原理 最近公共祖先&#xff08;Lowest Common Ancestors&#xff0c;LCA&#xff09;指有根树中距离两个节点最近的公共祖先。 祖先指从当前节点到树根路径上的所有节点。 u 和v 的公共祖先指一个节点既是u 的祖先&#xff0c;又是…

React基础之Context

前文有讲到Vue中的provide透传&#xff0c;祖孙组件之间通信。在react中也有类似的存在&#xff0c;他就是context&#xff0c;context的作用也是让祖孙组件之前通信。 React中&#xff0c;通过createContext方法来创建一个Context对象。 import React, { createContext } fr…

sqli-labs/Less-51

这一关的欢迎界面依然是以sort作为注入点 我们首先来判断一下是否为数字型注入 输入如下 sortrand() 对尝试几次 发现页面并没有发生变化 说明这道题的注入类型属于字符型 然后尝试输入以下内容 sort1 报错了 报错信息如下 我们从报错信息可以知道这道题的注入类型属于单…

JS-基础

JavaScript 基础第一天 01 JavaScript介绍 1.1 JavaScript 注释 单行注释 符号&#xff1a;//作用&#xff1a;//右边这一行的代码会被忽略快捷键&#xff1a;ctrl / 块注释 符号&#xff1a;/* */作用&#xff1a;在/* 和 */ 之间的所有内容都会被忽略快捷键&#xff1a;s…

【附源码】计算机毕业设计JAVA移动在线点菜系统服务端服务端

【附源码】计算机毕业设计JAVA移动在线点菜系统服务端服务端 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1…

【Unity】TimeLine系列教程——编排剧情!

前言 我们经常会看到游戏中有很多“花里胡哨”的系统&#xff0c;比如这样&#xff1a; 火影忍着疾风传 碧之轨迹S技能 这种效果感觉上像播放视频一样&#xff0c;但是却能将游戏不同的敌人加到镜头里面&#xff0c;有时候甚至根据双方关系还会有不同的反馈。如果是做视频或…

基于DQN的强化学习 快速浏览(基础知识+示例代码)

一、强化学习的基础概念 强化学习中有2个主要的实体&#xff0c;一个是智能体(agent)&#xff0c;另一个是环境(environment)。在强化学习过程中&#xff0c;智能体能够得到的是环境当前的状态(State)&#xff0c;即环境智能体所处环境当前的情况。另一个是上一步获得的环境的…

NePTuNe 论文笔记

NePTuNe:Neural Powered Tucker Network for Knowledge Graph Completion- Introduction- Background- Algorithm- Experiment- Conclusion- CodeShashank Sonkar, Arzoo Katiyar, Richard G.Baraniuk - Introduction 目前的链接预测方法&#xff1a; 张量因式分解&#xff1…

【服务器数据恢复】raidz多块硬盘离线的数据恢复案例

服务器数据恢复环境&#xff1a; 一台采用zfs文件系统的服务器&#xff0c;配备32块硬盘。 服务器故障&#xff1a; 服务器在运行过程中崩溃&#xff0c;经过初步检测没有发现服务器有物理故障&#xff0c;重启服务器后故障依旧&#xff0c;用户联系我们中心要求恢复服务器数据…

SpringBoot: Controller层的优雅实现

目录1. 实现目标2. 统一状态码3. 统一响应体4. 统一异常5. 统一入参校验6. 统一返回结果7. 统一异常处理8. 验证1. 实现目标 优雅校验接口入参响应体格式统一处理异常统一处理 2. 统一状态码 创建状态码接口&#xff0c;所有状态码必须实现这个接口&#xff0c;统一标准 pa…

Eolink 征文活动- -专为开发者设计的一款国产免费 API 协作平台

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; ▌背景 后端开发的程序员都需要有一个用得顺手的接口测试工具&#xff1b;以前&#xff0c;大家都喜欢用Google开发的一款接口测试工具postman来进行测试&#xff0c;…