数据结构——【堆】

news2024/11/14 3:12:07

一、堆的相关概念

1.1、堆的概念

1、堆在逻辑上是一颗完全二叉树(类似于一颗满二叉树只缺了右下角)。

2、堆的实现利用的是数组,我们通常会利用动态数组来存放元素,这样可以快速拓容也不会很浪费空间,我们是将这颗完全二叉树用层序遍历的方式储存在数组里的。

3、堆有两种分别是大根堆小根堆 。

1.2、堆的分类

1.2.1、大根堆

大根堆就是整个完全二叉树,任意一个根节点的值都比左右子树的值大    

          

这就是一个大根堆,所有根节点的值永远比左右子树的大,那么就可以看出,整棵树的根节点,他的值是整个堆中最大的。同时我们也发现没有直接父子关系的节点他们的值没有完全地关系,就像第二层的33和第三层的45以及20,没有规定第三层的元素值必须小于第二层,只要满足根节点比自己左右子树节点的值大即可。

1.2.3、小根堆

小根堆表示整个完全二叉树,任意一个根节点的值都比左右子树的值小

            

以上就是一个简单地小根堆它的定义与大根堆相似,只是跟节点的值小于自己的左右节点的值,同时小根堆的层与层之间没有直接关系的节点的值也没有必然的大小关系

1.3、堆的结构

堆的逻辑结构是一颗完全二叉树

堆的物理结构是一个数组

我们可以用左右孩子节点和父节点,来表示所有的节点。

leftchild = parent * 2 + 1;

rightchild = parent * 2 + 2;

parent = (child - 1) / 2;(child可以是左孩子,也可以是右孩子)

 如下图:是一个大根堆,父节点的值都大于子节点的值。

在数组中存储的是:

10865743

二、堆的实现

2.1、堆的功能

我们是以顺序表的形式实现的堆,其中基本的操作和顺序表的操作是大致一样的。

下面是我们要实现的堆的一些基础功能


#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>

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


//堆的初始化
void HeapInit(Heap* hp);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//打印堆
void HeapPrint(Heap* hp);
//交换函数
void Swap(HPDataType* p1, HPDataType* p2);

2.2、堆函数的实现

#include"Heap.h"

//初始堆
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}

// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);

	free(hp->a);
	hp->a = NULL;

	hp->size = hp->capacity = 0;
}

//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

//向上调整(前面的是堆)
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//小堆
		if (a[child] <  a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;

		}
		else
		{
			break;
		}
	}
}

//打印堆
void HeapPrint(Heap* hp)
{
	assert(hp);
	for (size_t i = 0; i < hp->size; i++)
	{
		printf("%d ",hp->a[i]);
	}
	printf("\n");
}

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		HPDataType* tem = (HPDataType*)realloc(hp->a,sizeof(HPDataType)*newcapacity);
		if (tem == NULL)
		{
			perror("malloc fail");
			exit(-1);
		}
		hp->a = tem;
		hp->capacity = newcapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;

	AdjustUp(hp->a, hp->size - 1);
}

//向下调整(大堆或者小堆)
void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//小堆
		if (child + 1 < n && a[child+1] < a[child])   //找到两个孩子节点较小的值
		{
			child++;
		}
		//建小堆
		if (a[child]<a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


// 堆顶元素的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->size>0);

	Swap(&hp->a[0], &hp->a[hp->size-1]);
	--hp->size;
	AdjustDown(hp->a, hp->size, 0); //大堆
}

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	assert(hp);
	assert(a);
	hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (hp->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->size = n;
	hp->capacity = n;
	memcpy(hp->a, a,sizeof(HPDataType) * n);

	//建堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(hp->a, i);
	}
}

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

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

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

2.3、堆的插入

堆是一个完全二叉树,在插入元素时是在堆的末尾插入的,但是为了把一个元素插入后,使这个堆还是一个堆,我们需要对堆中的数据尽心再次调整。

向上调整

我们插入一个元素是,在进行向上调整,把这个数放到合适的位置。我们来看看代码实现

void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//大堆
		if (a[child] >  a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;

		}
		else
		{
			break;
		}
	}
}

下面这张图帮助大家理解

接下来我们来实现堆的插入

我们还是和顺序表一样,相对其扩容情况进行讨论,当hp->size==hp->capacity时,证明没有多余空间了,我们需要增加空间,这里还是使用,realloc函数,将这个数插入进去后,对这个数进行向上调整,使之变成一个新堆。

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

	AdjustUp(hp->a, hp->size - 1);
}

2.4、堆的删除

向上调整

我们在堆中删除一个元素时删除的时堆顶元素,也就是第一个元素,我们一般会先让第一个元素和最后一个元素交换位置,然后hp->size--;为了让新的数据成为堆,我们将第一个数据向下调整,使之变成一个新堆。

我们来看看向下调整的代码该如何写:

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//小堆
		if (child + 1 < n && a[child+1] > a[child])   //找到两个孩子节点较大的值
		{
			child++;
		}
		//建小堆
		if (a[child]>a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

我们来看看这张图跟好的理解向下调整:

接下来我们来实现堆的删除

我们先考虑一下hp->size的临界问题,用一个断言就可以避免此类问题。

void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->size>0);

	Swap(&hp->a[0], &hp->a[hp->size-1]);
	--hp->size;
	AdjustDown(hp->a, hp->size, 0); //大堆
}

三、建堆

给一个数组我们如何把这个数组建成堆呢?

一般我们都有两种方法:

3.1、自顶向下(向上调整)

我们来看看代码如何实现

void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	assert(hp);
	assert(a);
	hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (hp->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->size = n;
	hp->capacity = n;
	memcpy(hp->a, a,sizeof(HPDataType) * n);

	//建堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(hp->a, i);
	}
}

我们使用错位相减的方式来计算 自顶向下(向上调整)的时间复杂度

时间复杂度:O(nlog(2)^n)

3.2、自低向上(向下调整)

我们来看看代码如何实现

void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	assert(hp);
	assert(a);
	hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (hp->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	hp->size = n;
	hp->capacity = n;
	memcpy(hp->a, a,sizeof(HPDataType) * n);

	//建堆
    //向下调整算法
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}
}

和自顶向下一样,还是错位相减来计算时间复杂度

时间复杂度:O(n)

四、堆的排序

我们学习堆,有一个很有用的地方,就是可以用堆进行排序,因为我们知道,大堆堆顶元素是数组中最小的,小队堆顶是数组中元素最小的。

当我们需要将一个数组进行从小到大的排序时:

1.将该数组建成一个大堆

2.第一个数和最后一个数交换,然后把交换的那个较大的数不看做堆里面

3.前n-1和数进行向下调整算法,选出大的数放到根节点,再跟倒数第二个交换......

 代码如下:

void HeapSort(int* a,int n)
{
	int i = 0;
	//这里用向下调整算法来建堆,因为时间复杂度只有O(N)
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&amp;a[0], &amp;a[end]);
		AdjustUp(a, end, 0);
		--end;
	}
}

时间复杂度:O(Nlog(2)^N)

五、topk问题

我们在做一些编程题会遇到一类问题,就是topk问题

topk问题指的有一组很大的数据,我们需要返回它最大(最小)的前K个元素。

这里我们就可以用堆排序很好的解决此类问题。

这里力扣平台有一个练习题,我们一起来看一看

面试题 17.14. 最小K个数 - 力扣(LeetCode)

思路:我们先建立一个大堆,先把前K个元素建成一个大堆,然后在将剩下的数和堆顶元素进行比较,如过大于堆顶数据,我们就和堆顶元素进行交换,然后将现在的堆顶元素向下调整,前k个数就是这组数据中最小的前K个数。

我们来看看该如何实现:

void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//大堆
		if (child + 1 < n && a[child+1] > a[child])   //找到两个孩子节点较小的值
		{
			child++;
		}
		//建大堆
		if (a[child]>a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

int* smallestK(int* arr, int arrSize, int k, int* returnSize)
{
    if(k==0)
    {
        *returnSize=0;
        return NULL;
    }
    int *ret=(int*)malloc(sizeof(int)*k);
    for(int i=0;i<k;i++)
    {
        ret[i]=arr[i];
    }


    //给前k个元素建大堆
    for(int i=(k-1-1)/2;i>=0;i--)
    {
        AdjustDown(ret, k, i);
    }

    for(int i=k;i<arrSize;i++)
    {
        if(ret[0]>arr[i])
        {
            ret[0]=arr[i];
            AdjustDown(ret,k,0);
        }
    }
    *returnSize=k;
    return ret;
}

六、大量数据中的topk问题

比如我们现在有100000个数据,我们要找到最大的10个数据,我们需要改怎么实现,还是利用topk解决,我们先将前100个数据建成一个小堆

//创建一个文件,并且随机生成一些数字
void CreateDataFile(const char* filename, int N)
{
	FILE* Fin = fopen(filename, "w");
	if (Fin == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}
	srand(time(0));
	for (int i = 0; i < N; i++)
	{
		fprintf(Fin, "%d ", rand() % 10000);
	}
}
void PrintTopK(const char* filename, int k)
{
	assert(filename);
	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}
	int* minheap = (int*)malloc(sizeof(int) * k);
	if (minheap == NULL)
	{
		perror("malloc fail");
		return;
	}
	//读前k个数
	for (int i = 0; i < k; i++)
	{
		//空格和换行默认是多个值之间的间隔
		fscanf(fout, "%d", &amp;minheap[i]);
	}
	//建k个数的堆
	for (int j = (k - 1 - 1) / 2; j >= 0; j--)
	{
		AdjustDown(minheap, j, k, cmp_down);
	}
	//读取后N-K个
	int x = 0;
	while(fscanf(fout,"%d",&amp;x)!=EOF)
	{
		if (x > minheap[0])
		{
			minheap[0] = x;
			AdjustDown(minheap, 0, k, cmp_down);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
	printf("\n");
	free(minheap);
	fclose(fout);
}
int main()
{
	//CreateDataFile("data.txt", 1000000);
	//找前10个最大的数
	PrintTopK("data.txt", 10);
	return 0;
}

这里只截取了一小部分

我们提前改10个较大的数,验证返回的正确错误。

返回的就是我们改的10个大的数字,证明返回正确,而且效率极其的高!

总结:堆的知识就介绍到这里,如有疑问或者质疑,请在评论区发出来,我会尽全力帮大家解决,成长的路上少不了你们的支持,你们的三连是我进步最大的动力,还望大佬们多多支持,一起加油!共勉!

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

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

相关文章

【Java】SpringData JPA快速上手,关联查询,JPQL语句书写

JPA框架 文章目录 JPA框架认识SpringData JPA使用JPA快速上手方法名称拼接自定义SQL关联查询JPQL自定义SQL语句 ​ 在我们之前编写的项目中&#xff0c;我们不难发现&#xff0c;实际上大部分的数据库交互操作&#xff0c;到最后都只会做一个事情&#xff0c;那就是把数据库中的…

电容 stm32

看到stm32电源部分都会和电容配套使用&#xff0c;所以对电容的作用产生了疑惑 电源 负电荷才能在导体内部自由移动&#xff0c;电池内部的化学能驱使着电源正电附近的电子移动向电源负极区域。 电容 将电容接上电池&#xff0c;电容的两端一段被抽走电子&#xff0c;一端蓄积…

【STL容器】vector

文章目录 前言vector1.1 vector的定义1.2 vector的迭代器1.3 vector的元素操作1.3.1 Member function1.3.2 capacity1.3.3 modify 1.4 vector的优缺点 前言 vector是STL的容器&#xff0c;它提供了动态数组的功能。 注&#xff1a;文章出现的代码并非STL库里的源码&#xff0c…

C++ PrimerPlus 复习 第三章 处理数据

第一章 命令编译链接文件 make文件 第二章 进入c 第三章 处理数据 文章目录 C变量的命名规则&#xff1b;C内置的整型——unsigned long、long、unsigned int、int、unsigned short、short、char、unsigned char、signed char和bool&#xff1b;如何知道自己计算机类型宽度获…

Jenkins Maven pom jar打包未拉取最新包解决办法,亲测可行

Jenkins Maven pom jar打包未拉取最新包解决办法&#xff0c;亲测可行 1. 发布新版的snapshots版本的jar包&#xff0c;默认Jenkins打包不拉取snapshots包2. 设置了snapshot拉取后&#xff0c;部分包还未更新&#xff0c;需要把包版本以snapshot结尾3. IDEA无法更新snapshots包…

超炫的开关效果

超炫的开关动画 代码如下 <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>Switch</title><style>/* 谁家好人 一个按钮写三百多行样式代码 &#x1f622;&#x1f622;*/*,*:after,*:before {box-sizing: bor…

代码随想录--栈与队列-用队列实现栈

使用队列实现栈的下列操作&#xff1a; push(x) -- 元素 x 入栈pop() -- 移除栈顶元素top() -- 获取栈顶元素empty() -- 返回栈是否为空 &#xff08;这里要强调是单向队列&#xff09; 用两个队列que1和que2实现队列的功能&#xff0c;que2其实完全就是一个备份的作用 impo…

产教融合 | 力软联合重庆科技学院开展低代码应用开发培训

近日&#xff0c;力软与重庆科技学院联合推出了为期两周的低代码应用开发培训课程&#xff0c;来自重庆科技学院相关专业的近百名师生参加了此次培训。 融合研学与实践&#xff0c;方能成为当代数字英才。本次培训全程采用线下模式&#xff0c;以“力软低代码平台”为软件开发…

聚焦真实用例,重仓亚洲,孙宇晨畅谈全球加密新格局下的主动变革

「如果能全面监管&#xff0c;加密行业仍有非常大的增长空间。目前加密用户只有 1 亿左右&#xff0c;如果监管明朗&#xff0c;我们可以在 3-5 年内获得 20-30 亿用户。」—— 孙宇晨 9 月 14 日&#xff0c;波场 TRON 创始人、火币 HTX 全球顾问委员会成员孙宇晨受邀出席 20…

RHCSA的一些简单操作命令

目录 1、查看Linux版本信息 2、ssh远程登陆 3、解析[zxlocalhost ~]$ 与 [rootlocalhost ~]# 4、退出命令exit 5、su——switch user 6、打印用户所处的路径信息pwd 7、修改路径 8、输出文件\目录信息 9、重置root账号密码 10、修改主机名称 1&#xff09;临时修改…

SkyWalking入门之Agent原理初步分析

一、简介 当前稍微上点体量的互联网公司已经逐渐采用微服务的开发模式&#xff0c;将之前早期的单体架构系统拆分为很多的子系统&#xff0c;子系统封装为微服务&#xff0c;彼此间通过HTTP协议RESET API的方式进行相互调用或者gRPC协议进行数据协作。 早期微服务只有几个的情况…

三种方式部署单机版Minio,10行命令干就完了~

必要步骤&#xff1a;安装MinIO 拉取MinIO镜像 docker pull quay.io/minio/minio 创建文件挂载点 mkdir /home/docker/MinIO/data &#xff08;文件挂载点映射&#xff0c;默认是/mydata/minio/data&#xff0c;修改为/home/docker/MinIO&#xff0c;文件存储位置自行修改&…

随笔-嗨,中奖了

好久没有动笔了&#xff0c;都懒惰了。 前段时间&#xff0c;老妹凑着暑假带着双胞胎的一个和老妈来了北京&#xff0c;听着小家伙叫舅舅&#xff0c;还是挺稀奇的。周末带着他们去了北戴河&#xff0c;全家人都是第一次见大海&#xff0c;感觉&#xff0c;&#xff0c;&#…

qiankun 乾坤主应用访问微应用css静态图片资源报404

发现static前没有加我指定的前缀 只有加了后才会出来 解决方案: env定义前缀 .env.development文件中 # static前缀 VUE_APP_PUBLIC_PREFIX"" .env.production文件中 # static前缀 VUE_APP_PUBLIC_PREFIX"/szgl" settings文件是封了一下src\settings…

测试平台前端部署

这里写目录标题 一、前端代码打包1、打包命令2、打包完成后,将dist文件夹拷贝到nginx文件夹中3、重新编写default.conf4、将之前启动的容器进行停止并且删除,再重新创建容器5、制作Dockerfile二、编写Dockerfile一、前端代码打包 1、打包命令 npm run build2、打包完成后,…

Kubernetes学习篇之组件

Kubernetes学习篇之组件 文章目录 Kubernetes学习篇之组件前言概述控制平面组件(Control Plane Components)kube-apiserveretcdkube-schedulerkube-controller-managercloud-controller-manager Node 组件kubeletkube-proxy容器运行时(Container Runtime) 插件(Addons)DNSWeb界…

驱动开发,IO多路复用实现过程,epoll方式

1.框架图 被称为当前时代最好用的io多路复用方式&#xff1b; 核心操作&#xff1a;一棵树&#xff08;红黑树&#xff09;、一张表&#xff08;内核链表&#xff09;以及三个接口&#xff1b; 思想&#xff1a;&#xff08;fd代表文件描述符&#xff09; epoll要把检测的事件…

Ubuntu安装深度学习环境相关(yolov8-python部署)

Ubuntu安装深度学习环境相关(yolov8-python部署) 本文将从如下几个方面总结相关的工作过程&#xff1a; Ubuntu系统安装(联想小新pro16) 2.显卡驱动安装3.测试深度学习模型 1. Ubunut 系统安装 之前在台式机上安装过Ubuntu&#xff0c;以为再在笔记本上安装会是小菜一碟&…

Linux内核源码分析 (B.x)Linux内存布局

一、32位系统的内存布局 为什么要将进程地址空间划分成内核空间和用户空间&#xff1f; 这个和处理器的体系结构有关。比如X86分为ring0~ring3级别&#xff0c;ring0给内核空间使用&#xff0c;ring3给用户空间使用&#xff1b;同样的&#xff0c;ARMv7也是如此&#xff0c;svc…

openGauss学习笔记-70 openGauss 数据库管理-创建和管理普通表-查看表数据

文章目录 openGauss学习笔记-70 openGauss 数据库管理-创建和管理普通表-查看表数据70.1 查询数据库所有表的信息70.2 查询表的属性70.3 查询表的数据量70.4 查询表的所有数据70.5 查询字段的数据70.6 过滤字段的重复数据70.7 查询字段为某某的所有数据70.8 按照字段进行排序 o…