启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——堆的应用之堆排、Top-K问题

news2025/4/3 0:00:01

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

人无完人,持之以恒,方能见真我!!!
共同进步!!

文章目录

  • 一、堆排引入之使用堆排序数组
  • 二、真正的堆排
    • 1.向上调整算法建堆
    • 2.向下调整算法建堆
    • 3.向上和向下调整算法建堆时间复杂度比较
    • 4.建堆后的排序
    • 5.堆排序和冒泡排序时间复杂度以及性能比较
  • 三、Top-K问题

一、堆排引入之使用堆排序数组

我们之前说过,堆除了是一个完全二叉树之外,还有一个重要特性就是,它的每一颗子树的根节点都是整颗子树的最大或最小值,那么一提到它的这个特性,我们可以想到什么呢?

是不是我们会自然而然的想到使用堆进行排序,在讲解使用堆进行排序之前说明一下,一般我们排序都是对数组进行排序,所以我们要先将数组中的内容先全部入堆,处理完之后再放入数组中

那么是不是我们每次使用堆进行排序必须先写一个堆,把数组中的内容放入堆,排完序之后再把数据放回数组呢?其实并不会,这个问题到后面真正的堆排部分就知道了,现在先暂时留留悬念
   现在我们还是继续刚刚的思路,毕竟现在属于堆排的引入部分,我们要先知道用堆进行排序的大致逻辑后,才能将真正的堆排理解透彻

当前的思路就是,我们将数组中的数据放入堆中,然后循环地取堆顶、出堆顶元素,每取到一次堆顶就放回数组中,直到堆为空,这样我们每次都可以从当前堆中取到最值,将它们依次放回数组自然就可以实现排序

接下来我们以升序为例,来画个简图模拟一下我们上面的思路,如下:

在这里插入图片描述

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

根据上面的示意图,我们大致了解了整个使用堆进行排序的过程,可以发现确实可以使用堆来进行排序,那么有了思路我们就开始动手写代码:

//使用现有堆进行排序
void HeapSort(int* arr, int n)
{
	HP hp;
	HPInit(&hp);
	size_t i = 0;//记录下标
	for (i = 0; i < n; i++)
	{
		//循环入堆
		HPPush(&hp, arr[i]);
	}
	//循环取堆顶数据
	i = 0;//重置下标
	while (!HPEmpty(&hp))
	{
		//堆不为空,取出堆顶数据放入数组
		int top = HPTop(&hp);
		arr[i++] = top;
		HPPop(&hp);
	}
	//销毁
	HPDestroy(&hp);
}

void Print(int* arr, int n)
{
	for (size_t i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main()
{
	int arr[] = { 4,10,24,19,1,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	
	HeapSort(arr, sz);
	Print(arr, sz);
	return 0;
}

那么我们来看看上面代码的运行结果,看看能否排序成功:

在这里插入图片描述

可以看到,最后我们成功将数组排成了降序序,但是我们也反复强调,这并不是真正的堆排,只是引出堆排的一种思路,因为我们如果直接使用堆这种数据结构进行排序的话,每次都必须写一个堆来辅助排序,太麻烦了

所以真正的堆排是借助了堆的算法思想,直接对数组进行调整,而不需要专门写一个堆来辅助完成堆排,那么接下来我们就来学习一下真正的堆排

二、真正的堆排

真正的堆排并不是使用堆这种数据结构来实现排序,而是借鉴了堆的算法思想,可以让数组模拟为一个堆进行排序,虽然实现可能麻烦一点点,但是它的效率非常高,等到我们和冒泡排序进行对比就知道了,下面我们就开始正式学习

首先我们要对数组进行排序,那么大概率我们会拿到一个乱序的数组,我们需要通过调整,将这个乱序的数组模拟成为一个堆,因为堆的底层也是用数组存储的,我们要是能让数组模拟成为一个堆,就可以不必再写一个堆这样的数据结构辅助排序了

我们将一个乱序数组模拟成为一个堆的过程称为建堆,建堆有两种思路,当我们讲解完之后我们再来对比它们有什么区别

1.向上调整算法建堆

现在我们要对一个乱序的数组使用向上调整算法建堆,我们之前讲过的向上调整算法都是建立在原本的堆上,也就是在向上调整之前,除了最后一个元素以外,其它的元素已经构成一个堆了,是在堆的基础上向上调整

而我们现在要对一个乱序的数组建堆也要沿用这种思想,具体思路就是,将一个乱序数组看作一颗完全二叉树,默认从根节点开始,将每一个节点都看作需要调整孩子节点,都进行一次向上调整(如果不懂可以直接看下面画的图)

为什么说这样就可以让乱序数组成堆了呢?我们可以仔细分析一下,第一次将根当作要调整的节点时,由于只有它一个节点,所以默认它自己就能成一个堆

然后第二次将根的左孩子当作要调整的节点时,就将根节点加左孩子这颗小的子树调整成了一个堆,第三次将根的右孩子当作要调整的节点时,就将根节点加左右孩子这颗子树调整成了一个堆(如果不懂可以直接看下面画的图)

这样一个节点一个节点地调整,每调整一个节点,就让这个节点和上面的节点成一个堆,当我们将每个节点都这样调整一次后,就能得到一个堆,我们来画图理解一下(这里以建小堆为例):

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

根据上图的样例,应该能更好地理解向上建堆的思想了,这里我们总结一下规律,向上建堆的本质就是从根节点开始,每让一个节点向上调整一次,就能使得这个节点和前面的节点构成的小子树成堆

那么是不是我们将所有节点都向上调整之后,就能将整个乱序数组建成一个堆,那么接下来有了思路我们就可以来写对应的代码了,具体做法就是创建一个循环,从根节点开始,依次对每个节点进行向上调整,如下:

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

是不是看起来很简单呢?那么又是不是只能使用向上调整算法建堆呢?其实还可以使用向下调整算法来建堆,我们马上就来讲讲向下调整算法建堆

当然,学到这里我们已经建好堆了,如果想提前学一下将我们建好堆的数组进行最后的排序,可以先看建堆后的排序,没有看向下调整算法也不影响观看

2.向下调整算法建堆

在上面我们使用向上调整算法建堆了,就是将每个节点都当作孩子来向上调整,每调整一个节点,这个节点和之前的节点构成的小子树就能构成堆,到最后一个节点向上调整之后,整颗树就成了堆

那么我们能不能将每个节点当作父节点都进行一次向下调整呢?我们从最后一个节点往前开始向下调整,这样每向下调整一个节点,这个节点和之后的节点构成的小子树也能成堆,到堆顶节点向下调整之后,整颗树也就成了堆,可以自行

但是我们其实可以对上面的思路进行优化,因为最后一层的节点都是叶子节点,它们都没有孩子,自然就不能进行向下调整,所以我们要找到从下至上的,第一个可以作为父节点的节点

其实也不难想到,这个节点就是最后一个节点的父节点,最后一个节点的父节点一定是从下至上的,第一个可以作为父节点的节点

那么有了思路之后我们就可以来直接写出代码了,至于整个向下调整算法建堆的示例图可以自行去挑战一下,只有自己能把整个过程画出来了之后,才是真正懂了向下调整算法建堆,那么这里我们直接给出代码:

//向下调整算法建堆
//n-1是最后一个节点,(n-1-1)/2是最后一个节点的父节点
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
	AdjustDown(arr, i, n);
}

3.向上和向下调整算法建堆时间复杂度比较

我们先来简单看看一次向上调整和向下调整的时间复杂度,按照最坏的情况来算,一次向上或向下调整都要调整满二叉树的层数次,我们之前说过二叉树的层次为log2(n+1),所以我们可以得出,一次向上或向下调整的时间复杂度为O(log N)

我们向上调整建堆和向上调整建堆的外层循环大致都是一个n,所以我们猜测向上调整建堆的时间复杂度大致为O(N * log N)

那么它们之间是否就是没有区别呢?随意使用哪个都可以吗?其实并不是,向上调整算法建堆的时间复杂度确实为O(N * log N),但是向下调整算法建堆的时间复杂度其实是O(N),要更快一些
   那么是为什么呢?这里我们给出计算向下调整算法建堆时间复杂度的证明过程,需要用到高中学过的错位相减,至于向上调整算法建堆的证明过程可以参照这个方式,可以自行下去证明,那么现在我们直接给出向下调整算法建堆时间复杂度的证明过程,如下:

在这里插入图片描述

根据上面的证明,我们可以得出向下调整算法建堆的时间复杂度确实为O(N),而不是O(N * log N),向上调整算法建堆的证明可以自行参照实现一下,最后结果为O(N * log N)

所以最后我们得出结论,向下调整算法建堆优于向上调整算法建堆

4.建堆后的排序

当我们使用向上和向下调整算法建堆之后,我们现在需要对它进行最后的排序,具体的思路不难,但是可能有些抽象,需要我们细细分析,画画图

首先在引入部分我们是利用现有的数据结构堆进行排序,将堆顶取出放回数组,再出堆顶数据,直到堆为空,但是现在我们只有一个成堆的数组,该怎么操作呢?

核心其实还是一样的,我们可以在数组中模拟取堆顶数据,出堆顶数据,反复拿到最值,具体方法就是:

交换根节点和最后一个节点,此时最后一个节点就是最大或最小的值,随后对新的堆顶元素进行向下调整,注意向下调整时要除开最后一个节点,让剩下的节点继续成堆

然后就是交换根节点和倒数第二个节点,此时倒数第二个节点就是第二大或第二小的值,随后又对新的堆顶元素进行向下调整,此时向下调整时要除开倒数第二个节点,让剩下的元素继续成堆

随后重复进行以上步骤,直到将整个数组排成有序,可能这样纯文字不好描述,我们来画图理解一下,这里还是以小根堆为例:

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

根据上图的演示,我们应该能够猜到如果我们把后面的操作做完会发生什么,应该会将整个数组排成降序,因为每次都将当前堆的最小值往后面拿,那么大的都在前面了,小的都在后面的,自然就成为了降序

那么我们如何把上面的功能写成代码呢?这里的难点就是如何每次都调整和交换时都不去影响已经排序好的数据,这里就直接给出思路

我们可以定义一个变量end作为下标来划分这个数组,在end之前的节点是还未处理好的节点,end指向的节点就是要交换的节点位置,end之后的节点是已经挪动过的、处理好的节点,如图:

在这里插入图片描述

此时数组都是还未处理的节点,让end指向最后一个节点处,表示end处的节点是要交换的节点,它之后还没有已经处理好的节点,之前都是未处理的节点,随后交换根节点和end位置的节点,如图:

在这里插入图片描述

然后将end作为向下调整时的堆中的数据个数,那么就只会调整end位置之前的节点,不会影响end位置的节点,如图:

在这里插入图片描述

那么有了整个建堆后的排序思路,我们接下来就直接写出代码,如下:

int end = n - 1;
while (end > 0)
{
	Swap(&arr[0], &arr[end]);
	AdjustDown(arr, 0, end);
	end--;
}

那么整个完整的堆排就已经完成了,完整代码为:

void HeapSort(int* arr, int n)
{
	//向上调整建堆
	/*for (int i = 0; i < n; i++)
	{
		AdjustUp(arr, i);
	}*/

	//向下调整算法建堆
	//n-1是最后一个节点,(n-1-1)/2是最后一个节点的父节点
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}

我们来看看代码的运行结果:

在这里插入图片描述

  • 我们建小堆会将数组排成降序,建大堆会将数组排成升序,因为最值都是放在最后的

  • 堆排的时间复杂度为O(N * log N),建堆的时间复杂度为O(N * log N)或O(N),建堆后的排序的时间复杂度为O(N * log N),所以总体下来堆排的时间复杂度大致为O(N * log N)

  • 由于我们将数组建堆后还是需要使用向下调整算法来进行最后的排序,再加上向下调整算法建堆又更快,所以在实现堆排时,我们往往只写一个向下调整算法,建堆和建堆后的排序都用它

5.堆排序和冒泡排序时间复杂度以及性能比较

那么现在我们新学了堆排这种排序方式,我们来对比一下堆排和冒泡排序,首先它们的时间复杂度一个为O(N * log N),一个为O(N^2)

我们来看看它们的差距到底有多大,我们来写一个代码随机生成10万个整型数据,看看最后它们分别用时多少,代码如下:

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

void TestOP()
{
    srand((unsigned int)time(NULL));
    const int N = 100000;
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);

    for (int i = 0; i < N; ++i)
    {
        a1[i] = rand();
        a2[i] = a1[i];
    }

    int begin1 = clock();
    HeapSort(a1, N);
    int end1 = clock();

    int begin2 = clock();
    BubbleSort(a2, N);
    int end2 = clock();

    printf("HeapSort:%d\n", end1 - begin1);
    printf("BubbleSort:%d\n", end2 - begin2);
    free(a1);
    free(a2);
}

int main()
{
    TestOP();
    return 0;
}

为了保证测试出最真实的数据,我们要把debug版本调成release版本,然后运行代码,我们来见证一下最后排序的结果,如图:

在这里插入图片描述

整整10万个数据堆排只需要5毫秒,而冒泡排序则需要7秒多,差距达到了上千倍,所以堆排其实是很快的,是最优秀的几个排序算法之一,至于其他的排序算法我们在后面还会介绍

三、Top-K问题

在解决TOP-K问题之前,我们要了解TOP-K问题是什么,它是求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤,⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等

那么我们能不能对这些数据直接进行排序呢?很明显是不现实的,因为有一些TOP-K问题的数据量非常大,我们不能都加载到内存中去排序,效率也不高

所以我们要想一个办法,让我们能够在内存被限制到很小的情况下,也能找出大量数据结合中前K个最⼤的元素或者最⼩的元素,那么到底怎么解决呢?
   我们现在来假设一个场景来辅助我们讲解,假设要求我们只能开k个内存空间,有10万个整型数据都存放在外存的磁盘文件上,我们要找到磁盘文件中前K个最大的数,这种情况下我们使用堆来解决

在我们着手来解决这个问题前,我们要先把环境模拟出来,就是要在我们当前目录下生成一个有10万个随机整数的文件,这里直接给出造数据文件的函数代码,这只是环境模拟,不是重点,重点是我们之后的TOP-K,造数据函数代码如下:

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

void CreateNDate()
{
	// 造数据
	int n = 100000;
	srand((unsigned int)time(NULL));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < n; ++i)
	{
		int x = (rand() + i) % 1000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

上面就是造数据的函数的代码了,注意,这个代码只需要运行一次就可以造出我们想要的文件,随后注释掉就好了,环境模拟好之后我们就进入真正的Top- K问题的解决了

首先我们要知道k是多少,它最好由用户决定,所以我们可以使用scanf来读取k,接着我们就来详细聊聊解决Top-K问题的基本思路:

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

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

相关文章

forms实现俄罗斯方块

说明&#xff1a; 我希望用forms实现俄罗斯方块 效果图&#xff1a; step1:C:\Users\wangrusheng\RiderProjects\WinFormsApp2\WinFormsApp2\Form1.cs using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms;namespace WinFor…

PHP回调后门

1.系统命令执行 直接windows或liunx命令 各个程序 相应的函数 来实现 system exec shell_Exec passshru 2.执行代码 eval assert php代码 系统 <?php eval($_POST) <?php assert($_POST) 简单的测试 回调后门函数call_user_func(1,2) 1是回调的函数 2是回调…

实操自动生成接口自动化测试用例

​这期抽出来的问题是关于如何使用Eolinker自动生成接口自动化测试用例&#xff0c;也就是将API文档变更同步到测试用例&#xff0c;下面是流程的示例解析。 导入并关联API文档和自动化测试用例 首先是登陆Eolinker&#xff0c;可以直接在线使用。 进入流程测试用例详情页&am…

Python数据类型-dict

Python数据类型-dict 字典是Python中一种非常强大且常用的数据类型&#xff0c;它使用键-值对(key-value)的形式存储数据。 1. 字典的基本特性 无序集合&#xff1a;字典中的元素没有顺序概念可变(mutable)&#xff1a;可以动态添加、修改和删除元素键必须唯一且不可变&…

0301-组件基础-react-仿低代码平台项目

文章目录 1 组件基础2 组件props3 React开发者工具结语 1 组件基础 React中一切都是组件&#xff0c;组件是React的基础。 组件就是一个UI片段拥有独立的逻辑和显示组件可大可小&#xff0c;可嵌套 组件的价值和意义&#xff1a; 组件嵌套来组织UI结构&#xff0c;和HTML一…

18-背景渐变与阴影(CSS3)

知识目标 理解背景渐变的概念和作用掌握背景渐变样式属性的语法与使用理解阴影效果的原理和应用场景掌握阴影样式属性的语法与使用 1. 背景渐变 1.1 线性渐变 运用CSS3中的“background-image:linear-gradient&#xff08;参数值&#xff09;;”样式可以实现线性渐变效果。 …

UE5学习记录part12

第15节&#xff1a; treasure 154 treasure: spawn pickups from breakables treasure是items的子类 基于c的treasure生成蓝图类 155 spawning actors: spawning treasure pickups 设置treasure的碰撞 蓝图实现 156 spawning actors from c &#xff1a; spawning our treas…

鸿蒙开发03样式相关介绍(一)

文章目录 前言一、样式语法1.1 样式属性1.2 枚举值 二、样式单位三、图片资源3.1 本地资源3.2 内置资源3.3 媒体资源3.4 在线资源3.5 字体图标3.6 媒体资源 前言 ArkTS以声明方式组合和扩展组件来描述应用程序的UI&#xff0c;同时还提供了基本的属性、事件和子组件配置方法&a…

一周掌握Flutter开发--9. 与原生交互(上)

文章目录 9. 与原生交互核心场景9.1 调用平台功能&#xff1a;MethodChannel9.1.1 Flutter 端实现9.1.2 Android 端实现9.1.3 iOS 端实现9.1.4 使用场景 9.2 使用社区插件9.2.1 常用插件9.2.2 插件的优势 总结 9. 与原生交互 Flutter 提供了强大的跨平台开发能力&#xff0c;但…

鸿蒙阔折叠Pura X外屏开发适配

首先看下鸿蒙中断点分类 内外屏开合规则 Pura X开合连续规则: 外屏切换到内屏,界面可以直接接续。内屏(锁屏或非锁屏状态)切换到外屏,默认都显示为锁屏的亮屏状态。用户解锁后:对于应用已适配外屏的情况下,应用界面可以接续到外屏。折叠外屏显示展开内屏显示折叠状态…

小程序中跨页面组件共享数据的实现方法与对比

小程序中跨页面/组件共享数据的实现方法与对比 在小程序开发中&#xff0c;实现不同页面或组件之间的数据共享是常见需求。以下是几种主要实现方式的详细总结与对比分析&#xff1a; 一、常用数据共享方法 全局变量&#xff08;getApp()&#xff09;、本地缓存&#xff08;w…

Java 大视界 -- 基于 Java 的大数据分布式计算在基因测序数据分析中的性能优化(161)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

DeepSeek-R1 模型现已在亚马逊云科技上提供

2025年3月10日更新—DeepSeek-R1现已作为完全托管的无服务器模型在Amazon Bedrock上提供。 2025年2月5日更新—DeepSeek-R1 Distill Llama 和 Qwen模型现已在Amazon Bedrock Marketplace和Amazon SageMaker JumpStart中提供。 在最近的Amazon re:Invent大会上&#xff0c;亚马…

Python数据可视化-第2章-使用matplotlib绘制简单图表

环境 开发工具 VSCode库的版本 numpy1.26.4 matplotlib3.10.1 ipympl0.9.7教材 本书为《Python数据可视化》一书的配套内容&#xff0c;本章为第2章 使用matplotlib绘制简单图表 本文主要介绍了折线图、柱形图或堆积柱形图、条形图或堆积条形图、堆积面积图、直方图、饼图或…

Redis 02

今天是2025/04/01 20:13 day 16 总路线请移步主页Java大纲相关文章 今天进行Redis 3,4,5 个模块的归纳 首先是Redis的相关内容概括的思维导图 3. 持久化机制&#xff08;深度解析&#xff09; 3.1 RDB&#xff08;快照&#xff09; 核心机制&#xff1a; 触发条件&#xff…

unity UI管理器

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events;// UI界面基类 public abstract class UIBase : MonoBehaviour {[Header("UI Settings")]public bool keepInStack true; // 是否保留在界面栈中public …

STRUCTBERT:将语言结构融入预训练以提升深度语言理解

【摘要】最近&#xff0c;预训练语言模型BERT&#xff08;及其经过稳健优化的版本RoBERTa&#xff09;在自然语言理解&#xff08;NLU&#xff09;领域引起了广泛关注&#xff0c;并在情感分类、自然语言推理、语义文本相似度和问答等各种NLU任务中达到了最先进的准确率。受到E…

16-CSS3新增选择器

知识目标 掌握属性选择器的使用掌握关系选择器的使用掌握结构化伪类选择器的使用掌握伪元素选择器的使用 如何减少文档内class属性和id属性的定义&#xff0c;使文档变得更加简洁&#xff1f; 可以通过属性选择器、关系选择器、结构化伪类选择器、伪元素选择器。 1. 属性选择…

SQL Server:用户权限

目录 创建 & 删除1. 创建用户命令整理创建 admin2 用户创建 admin_super 用户 2. 删除用户命令删除 admin2 用户删除 admin_super 用户 3. 创建时权限的区别admin2 用户权限admin_super 用户权限 查看方法一&#xff1a;使用对象资源管理器&#xff08;图形化界面&#xff…

服务器数据恢复—误格式化NTFS文件系统分区别慌,NTFS数据复活秘籍

NTFS文件系统下格式化在理论上不会对数据造成太大影响&#xff0c;但有可能造成部分文件目录结构丢失的情况。下面介绍一个人为误操作导致服务器磁盘阵列中的NTFS文件系统分区被格式化后的服务器数据恢复案例。 服务器数据恢复过程&#xff1a; 1、将故障服务器连接到一台备份…