[数据结构初阶]堆的应用

news2024/11/18 4:43:09

各位读者老爷好,鼠鼠又来了捏!鼠鼠上一篇博客介绍的堆,那么今天来浅谈以下堆的应用,那么好,我们先来看两个问题:

1.如果有一组乱序的数组数据,希望你将这组数组的数据排成升序或降序,该怎么排?

2.如果有1万个乱序的数据,希望你找出其中最大的前5个,该这么找到捏?


目录

1.堆排序

1.1.堆排序代码 

1.2.向下调整建堆法

 1.3.堆排序的优势

2.Top_K 问题

2.1.Top_K问题代码 

 2.2.办法3优势

3.ending


对于问题1,当然可以使用冒泡排序,但是冒泡排序的时间复杂度是O(N^2),显然不是一个很好的方法!鼠鼠我呀在这里介绍一个解决办法:堆排序!堆排序的时间复杂度是O(N*logN),相对于解决办法之冒泡排序好用的不是一星半点捏!

1.堆排序

要注意堆排序是对数组本身进行排序。上一篇博客中的运行结果似乎也将数据排好序了,但数组本身没有排好序,是额外开辟了空间取数组元素全部入堆后再循环取堆顶数据并删除堆顶数据而成的,这样子不是真正的堆排序。

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序

1.1.堆排序代码 

这篇博客我们以排降序为例讲解堆排序: 

本鼠先把堆排序(降序)的代码呈现如下,老爷们有兴趣看看啊:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef int HeapDataType;


//交换
void Swap(HeapDataType* a, HeapDataType* b)
{
	HeapDataType tmp = *a;
	*a = *b;
	*b = tmp;
}


//向上调整(小堆)
void AdjustUp(HeapDataType* a, int childcoordinate)
{
	int parentcoordinate = (childcoordinate - 1) / 2;
	while (childcoordinate > 0)
	{
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			childcoordinate = parentcoordinate;
			parentcoordinate = (parentcoordinate - 1) / 2;
		}
		else
		{
			break;
		}
	}
}


//向下调整(小堆)
void AdjustDown(HeapDataType* a, int parentcoordinate, int HeapSize)
{
	int childcoordinate = parentcoordinate * 2 + 1;
	while (childcoordinate < HeapSize)
	{
		if (a[childcoordinate] > a[childcoordinate + 1] && childcoordinate + 1 < HeapSize)
		{
			childcoordinate++;
		}
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			parentcoordinate = childcoordinate;
			childcoordinate = childcoordinate * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


//堆排序排降序
void HeapSort(HeapDataType* a, int HeapSize)
{
	int i = 0;

	//排降序,建小堆(向上调整建堆法)
	for (i = 1; i < HeapSize; i++)
	{
		AdjustUp(a, i);
	}
	int end = HeapSize - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
}


int main()
{
	int a[] = { 90,80,60,20,40,70,60,30,30,110 };
	int Size = sizeof(a) / sizeof(a[0]);
	printf("堆排序前:");
	for (int i = 0; i < Size; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	HeapSort(a, Size);
	printf("堆排序后:");
	for (int i = 0; i < Size; i++)
	{
		printf("%d ", a[i]);
	}
	return 0;
}

也许各位老爷对于这些个代码有点懵懵的,没关系,鼠鼠我来一点点分析上面代码:

首先我们来看这几个函数:

//交换
void Swap(HeapDataType* a, HeapDataType* b)
{
	HeapDataType tmp = *a;
	*a = *b;
	*b = tmp;
}


//向上调整(小堆)
void AdjustUp(HeapDataType* a, int childcoordinate)
{
	int parentcoordinate = (childcoordinate - 1) / 2;
	while (childcoordinate > 0)
	{
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			childcoordinate = parentcoordinate;
			parentcoordinate = (parentcoordinate - 1) / 2;
		}
		else
		{
			break;
		}
	}
}


//向下调整(小堆)
void AdjustDown(HeapDataType* a, int parentcoordinate, int HeapSize)
{
	int childcoordinate = parentcoordinate * 2 + 1;
	while (childcoordinate < HeapSize)
	{
		if (a[childcoordinate] > a[childcoordinate + 1] && childcoordinate + 1 < HeapSize)
		{
			childcoordinate++;
		}
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			parentcoordinate = childcoordinate;
			childcoordinate = childcoordinate * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

这几个函数上一篇博客已经介绍过了,分别是交换函数、向上调整函数和向下调整函数。只不过这里的向上调整函数和向下调整函数是服务小堆,而上一篇博客的向上调整函数和向下调整函数是服务大堆的,但是基本思想是不变的,这里鼠鼠就不多说了!

我们再来看下一个函数:

//堆排序排降序
void HeapSort(HeapDataType* a, int HeapSize)
{
	int i = 0;

	//排降序,建小堆(向上调整建堆法)
	for (i = 1; i < HeapSize; i++)
	{
		AdjustUp(a, i);
	}
	int end = HeapSize - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
}

这个函数就是堆排序函数了,排的是降序。参数a是待排序数组的指针,HeapSize是待排序数组(以下简称数组)的数据个数。

堆排序是这样子的:

 1.建堆。首先我们先将数组本身建成小堆。这里我们采用向上调整建堆法,用一个for循环调用向上调整函数将待排序数组本身变成小堆。这里本质模拟的是堆插入的过程建堆。

2.利用堆删除思想来进行排序。数组已经是小堆了,那么堆顶的数据一定是最小的数据,我们再将堆顶数据与数组尾部数据交换,这样子数组尾部数据就是最小的,再调用向下调整函数将数组前HeapSize-1个数据成小堆;然后我们再将由前HeapSize-1个数据构成的小堆的堆顶数据(这个数据是次小的)与由前HeapSize-1个数据构成的小堆的最后一个数据(也就是数组倒数第二个数据)交换,这样子数组倒数第二个数据就是次小的,然后我们再调用向下调整函数将数组前HeapSize-2个数据成小堆…………这样就可以将数组数据变成降序的。

我们看运行结果确实是没问题的:

1.2.向下调整建堆法

上面堆排序的代码中我们第一步是将待排序数组建成小堆,我们用的是向上调整建堆法,其实我们还可以用向下调整建堆法。

就像这样:

//堆排序排降序
void HeapSort(HeapDataType* a, int HeapSize)
{
	int i = 0;

	//排降序,建小堆(向下调整建堆法)
	for (i = (HeapSize-1-1)/2;i>=0;i--)
	{
		AdjustDown(a,i,HeapSize);
	}
	int end = HeapSize - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
}

向下调整建堆法的思想就是先找到以数组尾部数据的父节点(这个父节点下标假设为n)为根的树,将这颗树用向下调整函数调成小堆,再将以下标为n-1的节点为跟的树用向下调整函数调成小堆,再将以下标为n-2的节点为跟的树用向下调整函数调成小堆……再将以下标为0的节点为跟的树用向下调整函数调成小堆那么整个数组数据就成了一个小堆。

用向下调整建堆法的堆排序完整代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef int HeapDataType;


//交换
void Swap(HeapDataType* a, HeapDataType* b)
{
	HeapDataType tmp = *a;
	*a = *b;
	*b = tmp;
}


//向下调整(小堆)
void AdjustDown(HeapDataType* a, int parentcoordinate, int HeapSize)
{
	int childcoordinate = parentcoordinate * 2 + 1;
	while (childcoordinate < HeapSize)
	{
		if (a[childcoordinate] > a[childcoordinate + 1] && childcoordinate + 1 < HeapSize)
		{
			childcoordinate++;
		}
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			parentcoordinate = childcoordinate;
			childcoordinate = childcoordinate * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


//堆排序排降序
void HeapSort(HeapDataType* a, int HeapSize)
{
	int i = 0;
	//排降序,建小堆(向下调整建堆法)
	for (i = (HeapSize-1-1)/2;i>=0;i--)
	{
		AdjustDown(a,i,HeapSize);
	}
	int end = HeapSize - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
}


int main()
{
	int a[] = { 90,80,60,20,40,70,60,30,30,110 };
	int Size = sizeof(a) / sizeof(a[0]);
	printf("堆排序前:");
	for (int i = 0; i < Size; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	HeapSort(a, Size);
	printf("堆排序后:");
	for (int i = 0; i < Size; i++)
	{
		printf("%d ", a[i]);
	}
	return 0;
}

 运行结果是没问题的,跟上面用向上调整建堆法的堆排序结果一模一样。

这里用向下调整建堆法的堆排序是有以下优势的,所以我们写堆排序可以采用向下调整建堆法更佳:

1.堆排序的两个步骤都用向下调整函数即可,不必多写一个向上调整函数。

2.向上调整建堆法的时间复杂度是O(N*logN),而向下调整建堆法的时间复杂度是O(N),至于这些个复杂度为什么是这样子的鼠鼠就不证明了。

 1.3.堆排序的优势

堆排序的时间复杂度是O(N*logN),为啥复杂度是这个,鼠鼠不证明了!如果有100万个乱序数据让我们排序的话,笼统的说,我们用堆排序要排2千万次,如果用冒泡排序的话我们要排1万亿次。


对于问题2,我们可以用以下方法解决:

1.利用上一篇博客实现的堆,将1万个数据依次插入堆中构成大堆,再循环5次操作:取堆顶数据并删除堆顶数据。这样子取出来的5个堆顶数据就是最大的前5个数据。这个方法的时间复杂度是O(N*logN),空间复杂度是O(N)。

2.读取1万个数据的前5个数据,将这5个数据本身建立成小堆,再依次取剩余的9千9百9十5个数据与小堆堆顶数据比较,如果大于堆顶数据就替换堆顶数据并再次利用向下调整函数再成堆,比较完剩余的9千9百9十5个数据后,小堆里面的数据就是最大的前5个数。这种方法就引出鼠鼠要介绍的堆的应用之Top_K问题。

2.Top_K 问题

 TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大(就是说从N个数据中求最大或最小的前K个数据,N远大于K)。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1.如果求最大的前K个数据:

取前k个元素,建小堆。用剩余的N-K个元素依次与堆顶元素来比较,如果大于堆顶元素替换堆顶元素并利用向下调整函数再成堆,剩余N-K个元素比较完后,堆中剩余的K个元素就是最大的前K个数。

2.如果求最小的前K个数据:

取前k个元素,建大堆。用剩余的N-K个元素依次与堆顶元素来比较,如果小于堆顶元素替换堆顶元素并利用向下调整函数再成堆,剩余N-K个元素比较完后,堆中剩余的K个元素就是最小的前K个数。

2.1.Top_K问题代码 

思路我们解释完了,我们看一下代码:

鼠鼠用10万个随机数找出最大的前5个数为例子测试用堆来解决思路的可行性

#include<stdio.h>
#include<time.h>
#include<stdlib.h>

typedef int HeapDataType;

//交换
void Swap(HeapDataType* a, HeapDataType* b)
{
	HeapDataType tmp = *a;
	*a = *b;
	*b = tmp;
}


//向上调整(小堆)
void AdjustUp(HeapDataType* a, int childcoordinate)
{
	int parentcoordinate = (childcoordinate - 1) / 2;
	while (childcoordinate > 0)
	{
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			childcoordinate = parentcoordinate;
			parentcoordinate = (parentcoordinate - 1) / 2;
		}
		else
		{
			break;
		}
	}
}


//向下调整(小堆)
void AdjustDown(HeapDataType* a, int parentcoordinate, int HeapSize)
{
	int childcoordinate = parentcoordinate * 2 + 1;
	while (childcoordinate < HeapSize)
	{
		if (a[childcoordinate] > a[childcoordinate + 1] && childcoordinate + 1 < HeapSize)
		{
			childcoordinate++;
		}
		if (a[parentcoordinate] > a[childcoordinate])
		{
			Swap(&a[parentcoordinate], &a[childcoordinate]);
			parentcoordinate = childcoordinate;
			childcoordinate = childcoordinate * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


//找大的前K个数
void PrintfTop_K(HeapDataType* a, int Size, int K)
{
	HeapDataType* minheap = (HeapDataType*)malloc(sizeof(HeapDataType) * K);
	if (minheap == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int i = 0;
	//取a前K个数并建小堆
	for (i = 0; i < K; i++)
	{
		minheap[i] = a[i];
		AdjustUp(minheap, i);
	}
	int X = K;
	for (X; X < Size; X++)
	{
		if (a[X] > minheap[0])
		{
			minheap[0] = a[X];
			AdjustDown(minheap, 0, K);
		}
	}
	for (i = 0; i < K; i++)
	{
		printf("%d ", minheap[i]);
	}
	free(minheap);
}


int main()
{
	int Size = 100000;
	HeapDataType* a = (HeapDataType*)malloc(sizeof(HeapDataType) * Size);
	srand((unsigned int)time(0));
	for (int i = 0; i < Size; i++)
	{
		a[i] = (rand() + i) % 100000;
	}
	//打入5个探子,将这5个探子的值更改成大于100000
	a[9] = 110000;
	a[99] = 111000;
	a[999] = 111100;
	a[9999] = 111110;
	a[99999] = 11111111;
	PrintfTop_K(a, Size, 5);
	free(a);
	a = NULL;
	return 0;
}

看结果我们确实将5个探子找出来了,就是最大的前5个数:

 2.2.办法3优势

我们再分析以下:

对于问题2的解决办法1来说,我们都需要将数据加载到内存中才行 ,如果加载的数据量太大的话,这个方法就行不通。比如有1百亿个int类型的数据要全部加载到内存的话差不多要40GB。而且这个办法的时间复杂度和空间复杂度都不如办法2。

对于问题2的解决办法2来说,是最好的解决办法,首先时间复杂度是O(N*logK),一般K都很小,那么时间复杂度就是O(N),空间复杂度很明显是O(1)。其次,一般不用担心内存不足的问题。

3.ending

鼠鼠我才疏学浅,上面的博客难免有错误,恳请读者老爷发现后指出,鼠鼠我很期待各位老爷的斧正呢!

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

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

相关文章

BP算法的直观原理

这幅图非常清晰说明了BP算法的原理。 这幅图非常清楚展现了每个隐藏神经元权重关联的downstream。

vscode-task.json自定义任务

以下所有内容,参考自VScode官方文档: vscode_task-docs任务说明文档vscode_variables-reference-docs变量说明文档vscode addtional docs for task 说明: 博客内容均为个人理解,有错误请移步官方文档, 查阅文档, 纠正错误. 这篇blog记录一下个人对vscode任务(task)的使用方法 个…

【opencv】教程代码 —video(1) 对象追踪

CamShift算法、MeanShift追踪算法来追踪视频中的一个目标 camshift.cpp CamShift算法 // 引入相关的头文件 #include <iostream> // 包含C的输入输出流库 #include <opencv2/imgcodecs.hpp> // OpenCV图像编解码功能 #include <opencv2/imgproc.hpp> // Open…

Redis -- 缓存击穿问题

缓存击穿问题也叫热点Key问题&#xff0c;就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了&#xff0c;无数的请求访问会在瞬间给数据库带来巨大的冲击。 常见的解决方案有两种&#xff1a; 互斥锁 逻辑过期 逻辑分析&#xff1a;假设线程1在查询缓存之后&…

Java实现一个简易的布隆过滤器Bloom Filter

目录 什么是布隆过滤器&#xff1f; 作用&#xff1a; 实现一个简单的布隆过滤器&#xff1a; 解析&#xff1a; 什么是布隆过滤器&#xff1f; 布隆过滤器&#xff08;Bloom Filter&#xff09;是一种用于快速检查一个元素是否可能存在于一个集合中的数据结构&#xff0c;它…

【移动安全】对webview漏洞的一些分析

这次分析的app如下&#xff1a; 打开发现该app发现需要登录界面&#xff1a; 拖进jadx看一下&#xff0c;先来看一下AndroidManifest.xml文件 发现有两个类是导出&#xff0c;再来分析这两个类 这个RegistrationWebView类利用webview.loadUrl进行加载网页 java public class…

代码随想录第三十一天 | 贪心算法P1 | ● 理论基础 ● 455. ● 376. ● 53.

理论基础 题目分类 一般解题步骤 贪心算法一般分为如下四步&#xff1a; 将问题分解为若干个子问题找出适合的贪心策略求解每一个子问题的最优解将局部最优解堆叠成全局最优解 这个四步其实过于理论化了&#xff0c;我们平时在做贪心类的题目 很难去按照这四步去思考&…

Ps:合并到 HDR Pro

Ps菜单&#xff1a;文件/自动/合并到 HDR Pro Automate/Merge to HDR Pro 合并到 HDR Pro Merge to HDR Pro命令可以将同一场景的具有不同曝光度的多个图像合并起来&#xff0c;从而捕获单个 HDR 图像中的全部动态范围。 合并到 HDR Pro 命令分两步进行。 首先&#xff0c;需要…

什么是NLP?

&#x1f916;NLP是什么&#xff1f;&#x1f916; NLP&#xff08;Natural Language Processing&#xff09;&#xff0c;全称自然语言处理&#xff0c;是人工智能不可或缺的一环&#xff0c;它搭建了人与计算机之间沟通的桥梁&#x1f309;。 &#x1f6e0;️NLP强大功能一…

Day:003 | Python爬虫:高效数据抓取的编程技术(爬虫基础)

urllib发送get请求 在目前网络获取数据的方式有多种方式&#xff1a;GET方式大部分被传输到浏览器的html&#xff0c;images, js,css, … 都是通过GET 方法发出请求的。它是获取数据的主要方法 例如&#xff1a;www.baidu.com 搜索 Get请求的参数都是在Url中体现的,如果有中…

客户不报预算咋办?

谈崩了10个单子&#xff0c;我才领悟到谈判的精髓。创业一年了&#xff0c;去年累计服务客户30。说起来是市场好也罢&#xff0c;凑巧也罢反正没怎么费劲就谈下了不少客户&#xff0c;也做到了月入5位数。 今年一开年因为有老客户撑着&#xff0c;我也没太认真拓展新客户&#…

Mysql数据库getshell方法

今天摸鱼时候&#xff0c;突然有人问我不同的数据库getshell的方式&#xff0c;一时间我想到了mysql还有redis未授权访问到getshell的方式&#xff0c;但是仅仅第一时间只想到了这两种&#xff0c;我有查了查资料&#xff0c;找到了上面两种数据库getshell的补充&#xff0c;以…

Android源码笔记-输入事件(二)

这一节主要了解输入事件的获取&#xff0c;InputReaderThread继承自C的Thread类&#xff0c;Thread类封装了pthread线程工具&#xff0c;提供了与Java层Thread类相似的API。C的Thread类提供了一个名为threadLoop()的纯虚函数&#xff0c;当线程开始运行后&#xff0c;将会在内建…

【Linux】软硬链接 / 动静态库

目录 一. 软硬链接1. 硬链接2. 软链接3. unlink4. 目录的硬链接 二. 动静态库1.1 静态库制作1.2 静态库使用2.1 动态库制作2.2 动态库使用3. 动态链接原理 一. 软硬链接 1. 硬链接 硬链接(hard link) 可以将它理解为原始文件的别名, 和原始文件使用相同的 inode 编号和 data …

GraphSage

背景 大型图中节点的低维嵌入在各种预测任务中非常有用。GraphSage是一种通用的归纳框架&#xff0c;它利用节点特征信息&#xff08;例如&#xff0c;文本属性&#xff09;有效地为以前看不见的数据生成节点嵌入。相比于对每个节点训练单独的嵌入&#xff0c;GraphSage学习了一…

深入浅出 -- 系统架构之单体到分布式架构的演变

一、传统模式的技术改革 在很多年以前&#xff0c;其实没有严格意义上的前后端工程师之分&#xff0c;每个后端就是前端&#xff0c;同理&#xff0c;前端也可以是后端&#xff0c;即Ajax、jQuery技术未盛行前的年代。 起初&#xff0c;大部分前端界面很简单&#xff0c;显示的…

thinkphp6入门(21)-- 如何删除图片、文件

假设文件的位置在 /*** 删除文件* $file_name avatar/20240208/d71d108bc1086b498df5191f9f925db3.jpg*/ function deleteFile($file_name) {// 要删除的文件路径$file app()->getRootPath() . public/uploads/ . $file_name; $result [];if (is_file($file)) {if (unlin…

【语音识别】声学建模中基于树的状态绑定

01 基本想法 单音素HMM模型不能很好的应对自然说话人发音时的渐变过程&#xff0c;比如从一个音素转换到另一个音素时会存在协同发音现象。因此语音识别的先驱者提出了上下文建模概念&#xff0c;即使用中心音素&#xff08;单因素&#xff09;和前后两个音素组成三音素对每一…

【Laravel】09 用模型批量赋值简化代码 数据库关系

【Laravel】09 用模型批量赋值简化代码 & 数据库关系 1. 用模型批量赋值简化代码2. 数据库关系 1. 用模型批量赋值简化代码 原来存储一个值 2. 数据库关系 这里可以看到两个SQL是一样的

函数重载和引用【C++】

文章目录 函数重载什么是函数重载&#xff1f;函数重载的作用使用函数重载的注意点为什么C可以函数重载&#xff0c;C语言不行&#xff1f; 引用什么是引用&#xff1f;引用的语法引用的特点引用的使用场景引用的底层实现传参时传引用和传值的效率引用和指针的区别 函数重载 什…