数据结构 —— 堆

1.堆的概念及结构

堆是一种特殊的树形数据结构,称为“二叉堆”(binary heap)

看它的名字也可以看出堆与二叉树有关系:其实堆就是一种特殊的二叉树

堆的性质:

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

1.1大堆

大堆:

  • 大堆的根节点是整个堆中最大的数
  • 每个父节点的值都大于或等于其孩子节点的值
  • 每个父节点的孩子之间并无直接的大小关系

1.2小堆 

小堆:

  •  小堆的根节点是整个堆中最小的数
  • 每个父节点的值都小于或等于其孩子节点的值
  • 每个父节点的孩子之间并无直接的大小关系

 2.堆的实现

2.1使用数组结构实现的堆

由于堆是一个完全二叉树,所以堆通常使用数组来进行存储

使用数组的优点:

  • 相较于双链表更加的节省内存空间
  • 相较于单链表可以更好的算父子关系,并找到想要找的父子

2.2堆向上调整算法

堆的向上调整(也称为堆化、堆的修复或堆的重新堆化)是堆数据结构维护其性质的关键操作之一

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从最后一个叶子节点开始的向上调整算法可以把它调整成一个小堆
向下调整算法有一个前提:最后一个叶子之前是一个堆才能调整

 int arr = [ 15, 18, 19, 25, 28, 34, 65, 49, 37, 10]

小堆演示向上调整算法演示过程

向上调整的过程 :将新插入的值与它的父亲相比,如果小则向上调整,调整完成后与新的父亲去比较,直到其值 >= 父亲的时候停止调整 

void Swaps(HPDataType* a, HPDataType* b) {
	HPDataType temp;

	temp = *a;
	*a = *b;
	*b = temp;
}

//向上调整(小堆)
//child是下标

void AdjustUp(HPDataType* a, int child) {
	assert(a);

	int parent = (child - 1) / 2;//算父亲节点的下标

    //向下调整主要逻辑
	while (child > 0)     //当调整至根节点时,已经调整至极限,不用在调整
	{

        //当父亲节点 > 孩子时,开始调整
		if (a[parent] > a[child])     
		{

			Swaps(&a[child],&a[parent]);    //交换
			child = parent;                //走到新的位置为新一轮的向下调整做准备
			parent = (child - 1) / 2;     //算出新位置的父亲节点下标

		}

        //当父亲节点 < 孩子时,说明调整已经完毕,退出循环
		else
		{
			break;
		}

	}
}

2.3堆向下调整算法

在堆排序或其他需要维护堆性质的场景中,当堆的某个节点不满足堆的性质(对于最大堆,父节点小于其子节点;对于最小堆,父节点大于其子节点)时,就需要通过向下调整来修复这个子树,使其重新成为堆

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根结点开始的向下调整算法可以把它调整成一个小堆
向下调整算法有一个前提:左右子树必须是一个堆,才能调整
int array[] = {27,15,19,18,28,34,65,49,25,37};

 2.4堆的插入

堆的插入(HeapPush):通常通过将新元素添加到堆的末尾,并通过向上调整算法来维持堆的性质 (由于插入前的堆肯定是一个标准的堆,所以我们在将数据插入后执行一次向上调整算法,即可完成堆的插入)

2.5堆的删除

删除元素(HeapPop):在最大堆或最小堆中,通常删除的是根节点(即最大或最小元素),并通过向下调整算法来维持堆的性质 (由于删除前的堆肯定是一个标准的堆即左右子树肯定也是标准的堆,所以我们在将数据删除后执行一次向下调整算法,即可完成堆的删除)

为什么要删除根节点?

  • 相较于删除别的位置的节点,每次删除的根节点都是堆中最大或最小的数(大堆为最大,小堆为最小)、
  • 从根节点开始删除并调整堆结构,在实现上相对简便。只需删除后算法向下调整即可

2.6堆的代码实现

Heap.h

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

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

//堆的初始化
void HeapInit(Heap* php);
// 堆的销毁
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 AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int n, int parent);

Heap.c 

//堆的初始化
void HeapInit(Heap* hp) {
	assert(hp);

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

	free(hp->_a);
	hp->_capacity = hp->_size = 0;
	
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {
	assert(hp);

	//扩容
	if (hp->_size == hp->_capacity)
	{
		int newcapacity = hp->_capacity == 0 ? 2 : hp->_capacity * 2;
		HPDataType* newa = (HPDataType*)realloc(hp->_a, newcapacity * sizeof(HPDataType));
		if (newa == NULL)
		{
			perror("realloc");
			return;
		}
		hp->_capacity = newcapacity;
		hp->_a = newa;
	}

	//插入数据
	hp->_a[hp->_size] = x;
	hp->_size++;

	//向上调整
	AdjustUp(hp->_a,hp->_size-1);

}
void Swaps(HPDataType* a, HPDataType* b) {
	HPDataType temp;

	temp = *a;
	*a = *b;
	*b = temp;
}
//向上调整(小堆)
//child是数组的下标
void AdjustUp(HPDataType* a, int child) {
	assert(a);

	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (a[parent] > a[child])
		{
			Swaps(&a[child],&a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}

	}
}
// 堆的删除
void HeapPop(Heap* hp) {
	assert(hp);
	assert(hp->_size);

	//删除顶部数据  ,先与末尾的交换,在向下调整
	Swaps(&hp->_a[0],&hp->_a[hp->_size-1]);//让数组首元素,与尾元素交换位置
	hp->_size--;

	AdjustDown(hp->_a, hp->_size, 0);

}
//向下调整(小堆)
//n是数据数个数
void AdjustDown(HPDataType* a, int n, int parent) {
	assert(a);

	//假设法,默认两个孩子最小的是左孩子
	int child = parent * 2 + 1;

	//当没有左孩子的时候停止向下调整,拿新算的孩子位置去判断
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])//挑最小的孩子换,且要注意有没有右孩子
		{
			child += 1;
		}
		if (a[child] < a[parent])//孩子比父亲小就往上换
		{
			Swaps(&a[child], &a[parent]);
			parent = child;//孩子变成父亲,与他的孩子比
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}


}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp) {
	assert(hp);
	assert(hp->_size);

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

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

	return hp->_size == 0;
}

3堆的应用 — 堆排序 

堆排序,我们肯定是运用堆这个数据结构来完成我们的堆排序

接下来我们将充分的了解堆排序的运作原理

不难看出

  • 在每次交换时,堆顶最小的数都会沉到当前堆底
  • 小堆在经历过N(数据个数)轮后就会得到一个升序的数组
  • 大堆在经历过N(数据个数)轮后就会得到一个降序的数组

知道了堆排序的运转过程之后还有一个问题:使用者不可能说给你一个堆结构让你排序,肯定给的是一串无序且不是堆的数组给你排,这时侯我们就要考虑如何建堆了

3.1建堆

难道说建堆要用到上面写的堆结构,一个一个的去push吗?

其实不然,我们只需要使用向上调整算法向下调整算法就可以完成建堆

向上调整建堆法

1.构建过程

  • 初始时,将数组的第一个元素视为堆的根节点(对于下标从0开始的数组,根节点的下标为0)
  • 对于数组中剩余的元素(从下标1开始),将它们逐个视为“新插入”的元素,并执行向上调整操作
  • 在向上调整过程中,对于当前元素,首先计算其父节点的下标(parent = (child - 1) / 2)。然后,比较当前元素与其父节点的值
  • 如果当前元素的值大于其父节点的值(对于大根堆),则交换它们的位置。然后,将当前元素设置为新交换位置的父节点,并重复上述步骤,直到当前元素的值不大于其父节点的值或已经到达根节点
  • 通过重复上述步骤,直到所有元素都被处理过,最终得到的数组将满足堆的性质

2.时间复杂度

  • 向上调整建堆法的时间复杂度为O(N * logN),其中N是数组中的元素数量
void Swaps(int* a, int* b) {
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}

//向上调整(小堆)
void AdjustUp(int* a, int child) {
	assert(a);

	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (a[parent] > a[child])
		{
			Swaps(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}

	}
}

//堆排序
void HeapSort(int* a, int n) {

	//创建堆,向上调整建堆
	for (int i = 1; i < n; i++) 
	{
		AdjustUp(a,i);
	}


}

向下调整建堆法

向下调整(Adjust Down)是指从给定的非叶子节点开始,通过与其子节点比较并交换位(如果需要)来确保堆的性质

1.构建过程

  1. 确定开始位置
    • 对于长度为n的数组,由于堆是完全二叉树,所以最后一个非叶子节点的下标为(n-1-1)/2(整数除法)
    • 从这个下标开始,向前遍历所有非叶子节点
  2. 执行向下调整
  3. 遍历结束
    • 当所有非叶子节点都经过向下调整后,整个数组就形成了一个堆

2.时间复杂度

向下调整建堆法的时间复杂度为O(N),其中N是数组中的元素数量

void Swaps(int* a, int* b) {
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}
//向上调整(小堆)
void AdjustUp(int* a, int child) {
	assert(a);

	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (a[parent] > a[child])
		{
			Swaps(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}

	}
}

//堆排序
void HeapSort(int* a, int n) {

	//创建堆,向下调整建堆
	int parent = (n - 1 - 1) / 2;    //找到最后一个非叶子节点

	for (parent; parent >= 0; parent--)
	{
		AdjustDown(a, n, parent);
	}
	
	

}

3.2 利用堆删除思想来进行排序

void Swaps(int* a, int* b) {
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}

//向上调整(小堆)
void AdjustUp(int* a, int child) {
	assert(a);

	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (a[parent] > a[child])
		{
			Swaps(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}

	}
}

//向下调整(小堆)
void AdjustDown(int* a, int n, int parent) {
	assert(a);

	int child = parent * 2 + 1;

	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child += 1;
		}
		if (a[child] < a[parent])
		{
			Swaps(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}


}

//堆排序
void HeapSort(int* a, int n) {

	创建堆,向上调整建堆
	//for (int i = 1; i < n; i++)
	//{
	//	AdjustUp(a, i);
	//}

	//创建堆,向下调整建堆
	int parent = (n - 1 - 1) / 2;

	for (parent; parent >= 0; parent--)
	{
		AdjustDown(a, n, parent);
	}
	
	//小堆,可以排降序
	while (n)
	{
		Swaps(&a[0], &a[n - 1]);

		//交换完成把除了最后一个数据之外的数组看成一个新的堆,开始向下交换,形成新的小堆
		n--;
		AdjustDown(a, n, 0);

	}

}

4堆的应用 — Top-K问题

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能 数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前 K 个元素来建堆
  • k个最大的元素,则建小堆
  • k个最小的元素,则建大堆
2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

void Swaps(int* a, int* b) {
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整(小堆)大的下去
//n是数据数个数
void AdjustDown(HPDataType* a, int n, int parent) {
	assert(a);

	
	int child = parent * 2 + 1;

	
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child += 1;
		}
		if (a[child] < a[parent])
		{
			Swaps(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}


}
void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand((unsigned int)time(NULL));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void PrintTopK(int k) {

	//找出前K个最大的数

	//打开文件
	FILE* p = fopen("data.txt", "r");
	if (p == NULL)
	{
		perror("fopen error");
		return;
	}


	//构建一个小堆
	int x = 0;
	int arr[10] = { 0 };
	
	for (int i = k; i < 10; i++)
	{
		fscanf(p,"%d", &x);
		arr[i] = x;
	}

	//创建堆,向下调整建堆,F(N)
	int parent = (k - 1 - 1) / 2;

	for (parent; parent >= 0; parent--)
	{
		AdjustDown(arr, k, parent);//这里的n数组的位置,里面的child会算出超过数组的位置,这样会停下来
	}

	//在将后面的数字依次对比小堆顶部,比它大就向下调整
	while (fscanf(p, "%d", &x) > 0)
	{
		if (arr[0] < x)
		{
			arr[0] = x;
			AdjustDown(arr, k, 0);
		}
	}
	
	for (int i = 0; i < k; i++)
	{
		printf("%d\n", arr[i]);
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用 Vue 官方脚手架初始化 Vue3 项目

Vite 官网&#xff1a;https://cn.vitejs.dev/ Vue 官网&#xff1a;https://vuejs.org/ Vue 官方文档&#xff1a;https://cn.vuejs.org/guide/introduction.html Element Plus 官网&#xff1a;https://element-plus.org/ Tailwind CSS 官网&#xff1a;https://tailwindcss.…

0605 实际集成运算放大器的主要参数和对应用电路的影响

6.5.1 实际集成运放的主要参数 6.5.2 集成运放应用中的实际问题 6.5.2 集成运放应用中的实际问题

基于51单片机的简易温控水杯恒温杯仿真设计( proteus仿真+程序+设计报告+讲解视频)

基于51单片机的简易温控水杯恒温杯仿真设计( proteus仿真程序设计报告讲解视频&#xff09; 仿真图proteus7.8及以上 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;S0099 1. 主要功能&#xff1a; 基于51单片机的简易温控水杯恒温…

RV32A\CSR\Counters 指令集

RV32A\CSR\Counters指令集 一、RV32A指令集1、Load-Reserved/Store-Conditional InstructionsLR.WSC.W2、Atomic Memory OperationsAMOSWAP.WAMOADD.WAMOAND.WAMOXOR.WAMOOR.W二、CSR(Control and Status Register) 指令集CSRRWCSRRSCSRRCCSRRWICSRRSICSRRCI三、"Zicntr…

深圳建网站

深圳是中国最具活力和创新力的城市之一&#xff0c;也是全球网站建设行业蓬勃发展的重要市场之一。随着信息科技的不断发展和互联网的普及&#xff0c;越来越多的企业和个人意识到了建立网站的重要性&#xff0c;通过网站可以为企业带来更多的业务机会和营销渠道。 建立一个优质…

【OpenGL学习】OpenGL不同版本渲染管线汇总

文章目录 一、《OpenGL编程指南》第6版/第7版的渲染管线二、《OpenGL编程指南》第8版/第9版的渲染管线 一、《OpenGL编程指南》第6版/第7版的渲染管线 图1. OpenGL 2.1、OpenGL 3.0、OpenGL 3.1 等支持的渲染管线 二、《OpenGL编程指南》第8版/第9版的渲染管线 图2. OpenGL …

上新即爆品?2024小红书爆款黄金公式

5月&#xff0c;小红书正式上线了平台级新品营销IP——“宝藏新品”&#xff0c;旨在消费愈发审慎的当下&#xff0c;帮助品牌破除不确定性&#xff0c;达成新品的高质量生长。 本期千瓜将进一步解读「宝藏新品」策略&#xff0c;帮助品牌推新呈现更多样化的成长可能。 强种草…

单张图像扩散模型(Single Image DIffusion Model)

论文&#xff1a;SinDDM: A Single Image Denoising Diffusion Model&#xff0c; ICML 2023 去噪扩散模型&#xff08;DDM&#xff09;在图像生成、编辑和恢复方面带来了惊人的性能飞跃。然而&#xff0c;现有DDM使用非常大的数据集进行训练。在这里&#xff0c;介绍一个用于…

Qwen2 阿里最强开源大模型(Qwen2-7B)本地部署、API调用和WebUI对话机器人

阿里巴巴通义千问团队发布了Qwen2系列开源模型&#xff0c;该系列模型包括5个尺寸的预训练和指令微调模型&#xff1a;Qwen2-0.5B、Qwen2-1.5B、Qwen2-7B、Qwen2-57B-A14B以及Qwen2-72B。对比当前最优的开源模型&#xff0c;Qwen2-72B在包括自然语言理解、知识、代码、数学及多…

每日一练——有效的括号

20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 错误记录 #include<stddef.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h>typedef char STDataType;typedef struct Stack {STDataType* a;int capacity;int top; } Stack;vo…

【网络安全的神秘世界】磁盘空间告急?如何解决“no space left on device”的困扰

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 磁盘空间告急&#xff1f;如何解决“no space left on device”的困扰 &#x1f64b;‍♂️问题描述 错误信息 "write /var/lib/docker/tmp/GetIma…

理解数学概念——线性(线性性)

1. 线性相关词汇的词源 1.1 单词“line”的词源 这个单词是古英语“line”和古法语“ligne”二者的融合。在古英语中&#xff0c;“line”的词义为“缆绳&#xff0c;绳索&#xff1b;一系列&#xff0c;行&#xff0c;字母行&#xff1b;规则&#xff0c;方向(cable, rope; s…

【2024版】最新AI 大模型的掌握与运用技巧(非常详细)零基础入门到精通,收藏这一篇就够了

前言 曾经有一批强大的 AI模型摆在我面前&#xff0c;我却未曾珍惜&#xff0c;知道发现别人能够轻松驾驭它发挥巨大价值&#xff0c;才后悔莫及&#xff0c;如果上天给我重来一次的机会&#xff0c;我会努力学习经验和技巧&#xff0c;成为第一批熟练驾驭AI 模型的人! 随着 Ch…

可转债全部历史因子数据,提供api支持

今天在写可转债系统&#xff0c;顺便下载了一下服务器的可转债数据&#xff0c;给大家研究使用 from trader_tool.stock_data import stock_datafrom trader_tool.lude_data_api import lude_data_apiimport osclass convertible_bond_back_test_system: 可转债回测系统…

1秒揭秘:APP对接广告联盟,收益翻倍!

在当前数字时代&#xff0c;移动应用&#xff08;APP&#xff09;成为连接用户与服务的重要桥梁。 许多开发者通过开发APP并接入广告联盟&#xff0c;成功实现了收益的快速增长。 然而&#xff0c;对于初学者而言&#xff0c;从零开始开发一款能够有效对接广告联盟的APP&…

单源最短路径算法 -- 迪杰斯科拉(Dijkstra)算法

1. 简介 迪杰斯科拉&#xff08;Dijkstra&#xff09;算法是一种用于在加权图中找到最短路径的经典算法。它是由荷兰计算机科学家Edsger Wybe Dijkstra在1956年首次提出的&#xff0c;并以他的名字命名。这个算法特别适合于解决单源最短路径问题&#xff0c;即计算图中一个顶点…

在自己的电脑上搭建我的世界Java版服务器

很多朋友&#xff0c;喜欢玩Minecraft&#xff0c;也希望搭建一个服务器&#xff0c;用于和小伙伴联机&#xff1b; 并且&#xff0c;拥有服务器后&#xff0c;即使所有玩家都下线&#xff0c;“世界”依旧在运行&#xff0c;玩家可以随时参与其中&#xff0c;说不定一上线&am…

栈和队列(适配器模式模拟)

文章目录 声明stack的介绍queue的介绍deque双端队列简单介绍&#xff08;了解&#xff09;概述优缺点 适配器模式通过容器适配器模拟stack通过容器适配器模拟queue 总结 声明 模拟实现源代码已上传至gitee仓库&#xff1a;stack_queue_learn stack的介绍 stack文档介绍 sta…

go语言 | 快速生成数据库表的 model 和 queryset

就是生成 model 目录的 xxx.go 和 xxx_gen.go 文件 使用的工具&#xff1a; 快速生成 model&#xff1a;gentool&#xff1a;https://github.com/go-gorm/gen/tree/master/tools/gentool 根据 model 生成 queryset&#xff1a;go-queryset&#xff1a;https://github.com/jirfa…

开源大模型之辩:真假开源

目录 前言开源的定义什么是开源大模型&#xff1f;大模型时代首次出现闭源和开源“齐头并进”开源和闭源不是绝对对立的 大模型到底开源什么&#xff1f;传统开源软件与开源大模型的差别开源软件让开源大模型“受益匪浅” 不同大模型企业&#xff0c;开源、闭源策略不同开源…
最新文章