从零实现数据结构:堆的实现和简单堆排序

news2024/10/18 14:18:24

事先说明,这里采用的都是小堆。下面是代码中的小堆示意图

这里向大家分享一个常见数据结构可视化的网址:Data Structure Visualization (usfca.edu)

声明部分heap.h:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int HPDataType;

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

void HeapInit(hp* hp);
void HeapDestroy(hp* hp);
bool HeapEmpty(hp* hp);
HPDataType Heaptop(hp* hp);
int HeapSize(hp* hp);
void swap(HPDataType* x, HPDataType* y);
void AdjustUp(HPDataType* a, int child);
void HeapPush(hp* hp, HPDataType data);
void AdjustDown(HPDataType* a, int n);
void HeapPop(hp* hp);

函数实现部分heap.c:

初始化函数:

void HeapInit(hp* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

销毁函数:

void HeapDestroy(hp* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}

判空函数:

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

返回top元素函数:

HPDataType Heaptop(hp* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	return hp->a[0];
}

返回size函数:

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

交换函数:

void swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

向上调整函数:

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] > a[child]) // 小于关系确保最小堆
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

由于前面的代码比较简单,就不再做过多解释。这里注重讲解一下为什么要向上调整,以及这个思路是怎么来的。这里还是拿开头那个例子来举例:

当我们插入一个新的0的时候,这个时候就已经不满足小堆的定义了,我们需要对堆进行调整。

由于小堆要求父节点比所有的子节点都小,所以很自然的想法就是向上和父节点进行交换。你可能会问为什么不和其他的结点相比较,这是因为我们的目标是动最少的元素来完成,如果和其他结点进行交换剩下结点的父子关系就全部乱掉了,因此在不打乱其他结点的父子关系的唯一做法就是和自己的父节点进行比较。

当我们交换完成之后,发现依然不满足小堆的定义,所以很自然的想法是继续与父节点交换,然后向上调整的雏形就已经被我们构想出来了。

插入函数:

void HeapPush(hp* hp, HPDataType data)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size] = data;
	AdjustUp(hp->a, hp->size);
	hp->size++;
}

向下调整函数:

void AdjustDown(HPDataType* a, int n)
{
	int parent = 0;
	int leftchild = 2 * parent + 1;

	while (leftchild < n) // 确保左孩子在范围内
	{
		int minchild = leftchild;
		int rightchild = leftchild + 1;

		if (rightchild < n && a[rightchild] < a[leftchild])
		{
			minchild = rightchild; // 找到较小的子节点
		}

		if (a[parent] > a[minchild]) // 小于关系确保最小堆
		{
			swap(&a[parent], &a[minchild]);
			parent = minchild;
			leftchild = 2 * parent + 1; // 更新为新的左孩子
		}
		else
		{
			break;
		}
	}
}

同理这里向下调整也是一样,需要注意的是这里的写法,我们先是假设左边的结点是小的,然后再用判断。如果不这样做,则需要将父节点和两个子节点进行比较,这样会造成多余的比较次数。

还需要注意的是这行逻辑

if (rightchild < n && a[rightchild] < a[leftchild])

这里的逻辑是堆是完全二叉树,即使左节点存在右节点也有可能不存在,所以需要判断是否越界,并且这里的两个判断不能调换顺序,因为一旦交换,你就先越界访问了再进行的判断已经没有用了。

删除函数:

void HeapPop(hp* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	swap(&hp->a[0], &hp->a[hp->size - 1]); // 交换根和最后一个元素
	hp->size--;
	AdjustDown(hp->a, hp->size);
}

你可能还是会问为什么要一开始和根节点进行交换然后再进行调整,这还是因为我们的目标是动最少的元素完成。首先我们不能前后移动元素,因为一旦移动所有节点的父子关系就全部乱了,很有可能不满足原来的大堆或者小堆的定义了,所以我们能做的就只有交换元素。于是为了更少的交换我们直接和根结点进行交换。

还是举上面的例子来说明删除的逻辑:

原来的状态:

交换头尾元素:

比较大小,进行调整:

重复向下调整:

因此根据示意图,我们可以发现不管是向上调整还是向下调整都是最多调整高度次,按照二叉树的性质,实现复杂度也都是log2N.

完整实现代码:

#define _CRT_SECURE_NO_WARNINGS
#include "heap.h"
#include <assert.h>
#include <stdbool.h>


void HeapInit(hp* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

void HeapDestroy(hp* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}

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

HPDataType Heaptop(hp* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	return hp->a[0];
}

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

void swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] > a[child]) // 小于关系确保最小堆
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

void AdjustDown(HPDataType* a, int n)
{
	int parent = 0;
	int leftchild = 2 * parent + 1;

	while (leftchild < n) // 确保左孩子在范围内
	{
		int minchild = leftchild;
		int rightchild = leftchild + 1;

		if (rightchild < n && a[rightchild] < a[leftchild])
		{
			minchild = rightchild; // 找到较小的子节点
		}

		if (a[parent] > a[minchild]) // 小于关系确保最小堆
		{
			swap(&a[parent], &a[minchild]);
			parent = minchild;
			leftchild = 2 * parent + 1; // 更新为新的左孩子
		}
		else
		{
			break;
		}
	}
}

void HeapPush(hp* hp, HPDataType data)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("realloc fail");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size] = data;
	AdjustUp(hp->a, hp->size);
	hp->size++;
}

void HeapPop(hp* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	swap(&hp->a[0], &hp->a[hp->size - 1]); // 交换根和最后一个元素
	hp->size--;
	AdjustDown(hp->a, hp->size);
}

测试部分test.c:

#define _CRT_SECURE_NO_WARNINGS
#include "heap.h"

int main()
{
	hp heap;
	HeapInit(&heap);
	int arr[] = {0,1,3,2,5,6,7,4};
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		HeapPush(&heap, arr[i]);
	}
	while (!HeapEmpty(&heap))
	{
		printf("%d ", Heaptop(&heap));
		HeapPop(&heap);
	}
	return 0;
}

其实根据堆的定义来讲,即使你已经建好了小堆,但是仍然和有序存在一定距离,小堆唯一满足的有序就是根节点是最小的,所以我们如果要按照升序输出就只能先按照小堆建立,然后删除根结点再进行调整,这时根结点就已经是第二小的元素了,依次类推就拿到了升序的输出。

上面我们讲解了如何建一个大堆或者小堆,那么这里我们需要思考的问题就是如何拿到一个集合中,前k个最大的元素。例如我们要在10000个数中找到前10个最大的数,同时这也是下文所探讨的问题。

你可能会说:直接排个序,从大到小降序排下来前几名不就是TOPK了嘛

但是一般topk问题的使用环境都是大量甚至海量的数据中挑出最大的那么几十个甚至几个,这样的应用有很多,例如各种各样的排行榜就是如此。如果数据量大到一定的量级,无法一次性将所有的数据放入内存中进行比较,而且如果只是为了几个元素排所有元素的序就会造成很大量的计算资源浪费。所以在我们学习了大堆之后,就有了一个想法,那就是大堆的根结点是目前插入的元素中最大的。于是很自然的想法就是让这一万个数据一次插入建堆,每一次插入都能找到最大的数,插入完成之后,再进行pop九次就拿到了前十个最大的数。

但是这里我们既然都想到这了,就从看起来最笨的堆排序开始实现。

void HeapSort(int* a, int n)
{
	hp heap;
	HeapInit(&heap);
	for (int i = 0; i <n; i++)
	{
		HeapPush(&heap, a[i]);
	}
	int i = 0;
	while (!HeapEmpty(&heap))
	{
		int top = Heaptop(&heap);
		a[i++] = top;
		HeapPop(&heap);
	}
	HeapDestroy(&heap);
}
int main()
{
	hp heap;
	HeapInit(&heap);
	int arr[] = {0,1,3,2,5,6,7,4};
	HeapSort(arr, sizeof(arr) / sizeof(int));
	for (int i = 0; i < sizeof(arr) / sizeof(int);i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

我们可以看到的确是降序输出了,由于上一集我们采用的是小堆,而今天使用的是大堆,所以我们只需要将向上调整和向下调整的<>大于小于符号交换一下就能实现将大小堆互换。由此我们也可以看出堆排序的时间复杂度为n*logn。

但是我们可以看到这里的实现方式有点繁琐多余,我们需要另一个空间建堆,建完之后然后再重新拷贝到原数组,既消耗时间又消耗空间,于是我们想能不能直接在原数组上动手。

然后我们思考如何实现,于是我们再一次地从堆的本质出发,我们这里所谓的堆实际上在物理内存上的储存形式依旧还是数组,只不过我们将数组的下标和元素大小关系进行了一定规律的映射,从而达到逻辑上是一颗完全二叉树,物理上是一个数组的效果。于是这里又分出两种不同的建堆思路,一种是向上调整建堆,一种是向下调整建堆,我们依次来思考如何实现。

因此我们在这里就当在原数组上重新建一次堆,当我们拿到我们原来的数组时,我们就当这已经是一个堆了,只不过还需要进行向上调整而已。我们从第一个元素开始依次进行向上调整的操作,当我们只有一个元素的时候已经满足了大堆的定义了,当我们有第二个元素的时候不一定满足就向上调整,以此类推,当循环到数组末尾的时候,该数组已经完成了建堆的过程。接下来就是向下交换元素,于是我们有了实现:

void HeapSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

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

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

相关文章

Turtle画树丛

Turtle画树丛&#xff0c;50棵树&#xff0c;左侧的树向左倾斜&#xff0c;右侧的树向右倾斜。 完整代码如下&#xff1a; import turtle import randomdef draw_tree(pos,hd,angle,len,init_len,level): t.penup()t.goto(pos)t.pendown()t.setheading(hd)if pos[0]<0:t…

基于C++实现(控制台)职工信息管理系统

高级程序设计实验报告 一、实验内容 设计一个职工信息管理案例&#xff0c;实现不同类别职工的工资数据。职工的种类包括&#xff1a;正式职工和临时工。定义一个基本的职工类&#xff0c;基本信息包括&#xff1a;编号、姓名、性别、年龄、家庭住址、基本职务工资。派生出正…

当前python文件所在位置的上级文件夹的路径表示法:..的等价表示法os.pardir

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 当前python文件 所在位置的上级文件夹 的路径表示法&#xff1a;.. 的等价表示法 os.pardir [太阳]选择题 下列说法中正确的是? import os parent_dir os.pardir print("【显示】…

Axure重要元件三——中继器表单制作

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 本节课&#xff1a;中继器表单制作 课程内容&#xff1a;利用中继器制作表单 应用场景&#xff1a;台账、表单 案例展示&#xff1a; 步骤一&#xff1a;建立一个背景区…

【WPF】04 Http消息处理类

这里引入微软官方提供的HttpClient类来实现我们的目的。 首先&#xff0c;介绍一下官方HttpClient类的内容。 HttpClient 类 定义 命名空间: System.Net.Http 程序集: System.Net.Http.dll Source: HttpClient.cs 提供一个类&#xff0c;用于从 URI 标识的资源发送 HTTP 请…

探索性数据分析 (EDA) 简介

文章目录 一、介绍二、探索性数据分析 (EDA)三、探索性数据分析的类型3.1 单变量分析3.2 双变量分析3.3 多元分析 四、理解 EDA五、结论 一、介绍 探索性数据分析是研究或理解数据并提取洞察数据集以识别数据模式或主要特征的过程。EDA 通常分为两种方法&#xff0c;即图形分析…

Spring Boot 整合 RocketMQ 之顺序消息

前言&#xff1a; 上一篇我们分享了 Spring Boot 整合 RocketMQ 完成普通消息发送的过程&#xff0c;本篇我们来分享一下 RocketMQ 顺序消息的发送。 RocketMQ 系列文章传送门 RocketMQ 的介绍及核心概念讲解 Spring Boot 整合 RocketMQ 之普通消息 Spring Boot 整合 Rock…

大数据-178 Elasticsearch Query - Java API 索引操作 文档操作

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

芯片设计企业ERP软件如何选择更好

在芯片设计这一高科技领域&#xff0c;高效的企业管理成为推动创新与市场响应速度的关键。ERP(企业资源计划)软件作为企业管理的核心工具&#xff0c;其选择直接关系到企业的运营效率与竞争力。那么&#xff0c;芯片设计企业在面对琳琅满目的ERP软件时&#xff0c;如何做出更优…

【HTML + CSS 魔法秀】打造惊艳 3D 旋转卡片

HTML结构 box 类是整个组件的容器。item-wrap 类是每个旋转卡片的包装器&#xff0c;每个都有一个内联样式–i&#xff0c;用于控制动画的延迟。item类是实际的卡片内容&#xff0c;包含一个图片。 <template><div class"box"><div class"item…

Axure横向菜单高级交互

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;横向菜单高级交互 主要内容&#xff1a;横向菜单左右拖动、选中效果 应用场景&#xff1a;app横向菜单、pc后台动态区域 案例展示&#xff1a; 演…

ThreadLocal源码详解

目录 Thread、ThreadLocalMap 、ThreadLocal关系 ThreadLocal中的get、Set方法 ThreadLocal 内存泄露问题 Thread、ThreadLocalMap 、ThreadLocal关系 从源码可以看出&#xff1a;Thread类中有成员变量ThreadLocalMap&#xff0c;ThreadLocalMap类中有成员变量Entry[]数组&a…

Spring Cache Caffeine 高性能缓存库

​ Caffeine 背景 Caffeine是一个高性能的Java缓存库&#xff0c;它基于Guava Cache进行了增强&#xff0c;提供了更加出色的缓存体验。Caffeine的主要特点包括&#xff1a; 高性能&#xff1a;Caffeine使用了Java 8最新的StampedLock乐观锁技术&#xff0c;极大地提高了缓存…

buffer/cache内存优化_posix_fadvise_主动释放读缓存cache

1.问题现象 1.htop free命令发现系统 buffer/cache 内存占用高 free -h total used free shared buff/cache available Mem: 61Gi 15Gi 569Mi 1.7Gi 45Gi 43Gi Swap: 30Gi 0.0Ki 30Gi cat /proc/meminfo or grep -E "Buff|Cache" /proc/meminfo Buffers: 370568 kB …

Linux 进程终止和进程等待

目录 0.前言 1. 进程终止 1.1 进程退出的场景 1.2 进程常见退出方法 1.2.1 正常退出 1.2.2 异常退出 2. 进程等待 2.1 进程等待的重要性 2.2 进程等待的方法 2.2.1 wait() 方法 2.2.2 waitpid() 方法 2.3 获取子进程 status 2.4 阻塞等待和非阻塞等待 2.4.1 阻塞等待 2.4.2 非阻…

拼三角问题

欢迎来到杀马特的主页&#xff1a;羑悻的小杀马特.-CSDN博客 目录 一题目&#xff1a; 二思路&#xff1a; 三解答代码&#xff1a; 一题目&#xff1a; 题目链接&#xff1a; 登录—专业IT笔试面试备考平台_牛客网 二思路&#xff1a; 思路&#xff1a;首先明白能组成三角形…

php的echo和print输出语句⑥

在 PHP 中有两个基本的输出方式&#xff1a; echo 和 print。 echo 和 print 区别: echo : 可以输出一个或多个字符串 print : 只允许输出一个字符串。 提示&#xff1a;echo 输出的速度比 print 快&#xff0c; echo 没有返回值&#xff0c;print有返回值1。 <?php …

【赵渝强老师】Oracle的联机重做日志文件与数据写入过程

在Oracle数据库中&#xff0c;一个数据库可以有多个联机重做日志文件&#xff0c;它记录了数据库的变化。例如&#xff0c;当Oracle数据库产生异常时&#xff0c;导致对数据的改变没有及时写入到数据文件中。这时Oracle数据库就会根据联机重做日志文件中的信息来获得数据库的变…

Submariner 服务更新同步测试

测试服务更新同步问题 在集群1 部署 nginx1服务&#xff0c;导出服务&#xff0c;分配的虚拟 IP 为 100.1.255.253 在其他集群检测 serviceimport &#xff0c;可以检测到 nginx1 服务对应的 serviceimport 正常情况下的 serviceexport 如果删除 service 或者 删除 serviceexp…

OpenAI Canvas用户反馈:并不如外界传言般“炸裂”,更不是“AGI的终极交互形态” | LeetTalk Daily...

“LeetTalk Daily”&#xff0c;每日科技前沿&#xff0c;由LeetTools AI精心筛选&#xff0c;为您带来最新鲜、最具洞察力的科技新闻。 Canvas作为一个独立的界面&#xff0c;通过与ChatGPT的结合来提升用户的协作能力和创作效率。尽管用户对其独立性与现有工具的整合存在不同…