当前位置: 首页 > news >正文

选择排序(堆排序和topK问题)

选择排序

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

如果我们用扑克牌来举例,那么选择排序就像是提前已经把所有牌都摸完了,而再进行牌之间的排序;而插入排序则是边摸边排。

直接选择排序

  • 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

那么下面我先将代码展示给大家,然后再为大家讲解其中奥妙

void SelectSort(int* a, int n) {int begin = 0;int end = n - 1;while (begin < end) {int min = begin;int max = begin;for (int i = begin + 1; i <= end; i++) {if (a[i] < min) {min = i;}if (a[i] > max) {max = i;}}Swap(&a[begin], &a[min]);if (max == begin){max = min;}Swap(&a[end], &a[max]);begin++;end--;}
}

首先呢,我们先把起始位置的下标和最后位置的下标给记录下来,并将最小值和最大值的下标都初始化为begin,外面再套上一层循环,限制条件为begin<end,当两个下标走到一起或者错开时,就会结束循环,也就排好了。

而while循环里面的才是排序的逻辑部分,for循环从begin的下一个位置开始,到end的位置结束,并在其中进行比较,改变每一次循环中最大值和最小值的下标,并在循环结束后交换最小值和begin下标值的位置,最大值与end下标值的位置,最后begin和end都往中间走,开始下一轮循环

不过需要注意的是,我们加入了一个if判断语句:其实这是为了防止最大值就在begin下标时,原来的最大值会和最小值交换位置,然后最小值会被换到end的位置上成为最大值,那样子的话就会出现错误,排序便失败了;但加上这个之后,在第一次交换过后,max的值到了min的下标,这个时候只需要把max下标也改为min,这个时候替换就不会再把最小值给替换到最后,而是最大值了。这样讲可能也有点绕,给大家画个图便于理解。
在这里插入图片描述
相信大家根据函数看就可以看懂啦!还是很好理解的!

堆排序

相比于刚才的直接选择排序,想必当然还是堆排序更加吸引大家的注意,那就让我们开始学习吧!

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

代码如下~

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

虽然堆排序的本体很小,但是千万不能忽视了向上调整算法和向下调整算法,所以还是把这一串代码附在下面

void AdjustUp(int* a, int child) {int parent = (child - 1) / 2;while (child > 0) {if (a[child] > a[parent]) {Swap(&a[child], &a[parent]);}else {break;//这里没必要return}child = parent;parent = (parent - 1) / 2;}
}void AdjustDown(int* a, int size, int parent) {int child = parent * 2 + 1;//假设左子节点小于右子节点//右子节点不一定有,可能会越界while (child < size) {if (child + 1 < size && a[child + 1] > a[child]) {child++;//其实就是左子节点转换到右子节点上}if (a[child] > a[parent]) {Swap(&a[child], &a[parent]);parent = child;//parent移动到原来child的位置上child = child * 2 + 1;//child来寻找自己的下一个左子节点}else {break;}}
}

虽然堆排序中有堆,但是我们不可能真的建一个堆然后再进行排序,毕竟手搓一个堆的函数还是挺麻烦的,所以我们本质上是模拟堆插入的过程建堆,并利用其逻辑对数组中的元素进行排序,我们还是用例子说话。并且在建堆之前还有一个需要注意的,因为现在给的例子是以升序排列,所以我们现在建立的是大堆(需要在向上调整算法和向下调整算法中改变大于小于符号)

建立大堆的原因还有一个,那就是如果建立小堆的话,当删除堆顶元素(最小值)时,剩下的数还看作堆的话,关系就全乱了,需要重新建堆,浪费时间。

第一步:建堆
在这里插入图片描述
第二步:排序
其实就是将end定为数组的最后一个下标n-1,然后堆顶元素和最后一个元素交换,向下调整之后,删除最后一个元素,最后end走到0下标的时候就结束,写一两步大家看看
在这里插入图片描述
实际上,虽然在堆中删除了,但我们直到此时9已经到了n-1下标的位置,也就是排在了最大值的位置上。而向下调整之后,我们会发现,8又到了最上方,并且也是目前的最大值,也就是下一次,8会与2交换,成为次大的值;2又与7交换,2又与6交换,那么很明显,下一次循环交换的数就是7了,之后就是6,这样,最大值就慢慢的被调节到了end的位置,最后数组中的元素都正序排列。

优化

除了使用向上调整建堆,其实我们还可以使用向下调整建堆,进行讲解后,大家甚至还会发现向下调整更加简洁方便

for (int i = (n-1-1)/2; i >= 0; --i){AdjustDown(a, n, i);}

以上便是将向上调整改为向下调整算法后的函数,为什么从(n-1-1)/2开始呢,是因为n-1是最后一个元素的下标,而(n-1-1)/2则是找到其父节点,然后从底端进行调整。

而至于为什么向下建堆更简洁呢?给大家用数学写写,大家就懂啦!

在这里插入图片描述
在这里插入图片描述
eg.n=2^h-1上面的T(n)都是T(h),到下面才是T(n),写错了QAQ
由此可以看出,向下调整建堆的时间复杂度为O(n),下面我们计算向上调整建堆的时间复杂度

在这里插入图片描述
由此可知:向上调整建堆的时间复杂度为O(nlogn),是大于向下调整建堆的,这样子的话,我们以后如果使用堆排序,我们就可以直接忽略向上调整算法,只写向下调整算法,代码量可以更少,时间复杂度也更精简

topK问题

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

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
void PrintTopK(int* a, int n, int k) {int* heap = (int*)malloc(sizeof(int) * k);if (heap == NULL) {perror("malloc fail");return;}for (int i = 0; i < k; ++i) {heap[i] = a[i];AdjustUp(heap, i);//先用前k个数创建一个小堆}for (int i = k; i < n; ++i) {if (a[i] > heap[0]) {heap[0] = a[i];//从k下标开始遍历数据,如果大于heap[0],就让其成为heap[0]AdjustDown(heap, k, 0);//然后向下调整}}for (int i = 0; i < k; ++i) {printf("%d ", heap[i]);//打印最大的k个数}printf("\n");free(heap);//记住释放heap开辟的内存
}

我们还是照样举一个例子,虽然topK是在n大于k很多的情况下才使用的,但为了看上去简单,我们选择两个相近的n与k
在这里插入图片描述
我们在插入后进行了两次向下调整,由此可知,当我们进行完所有的向下调整之后,留在k个元素小堆中的元素一定是最大的几个

当然,除了从数组中取出的方法,我们还可以写出一种从文件中拿出数据并排序的topK函数,大家请看!

void CreateNDate()
{// 造数据int n = 10000000;  // 设置要生成的数据数量srand(time(0));  // 使用当前时间作为随机数种子,确保每次运行生成的随机数不同const char* file = "data.txt";  // 指定数据文件的名称FILE* fin = fopen(file, "w");  // 以写模式打开文件if (fin == NULL){perror("fopen error");  // 输出文件打开错误信息return;}// 随机生成n个整数,并将其写入文件for (int i = 0; i < n; ++i){int x = (rand() + i) % 10000000;  // 生成0到9999999之间的随机整数,加i是因为随机数最多只可以生成3万多个,会有重复的,这样能保证重复率大大降低fprintf(fin, "%d\n", x);  // 将随机数写入文件}fclose(fin);  // 关闭文件
}
void PrintTopK(const char* file, int k)
{FILE* fout = fopen(file, "r");  // 以只读模式打开文件if (fout == NULL){perror("fopen error");  // 输出文件打开错误信息return;}// 建一个k个数小堆int* minheap = (int*)malloc(sizeof(int) * k);  // 分配大小为k的整型数组内存if (minheap == NULL){perror("malloc error");  // 输出内存分配错误信息return;}// 读取前k个数,构建最小堆for (int i = 0; i < k; i++){fscanf(fout, "%d", &minheap[i]);  // 从文件中读取整数,构建最小堆AdjustUp(minheap, i);  // 执行向上调整,维护最小堆性质}int x = 0;while (fscanf(fout, "%d", &x) != EOF)  // 从文件中读取整数,直到文件结束{if (x > minheap[0])  // 如果当前数字大于堆顶元素{minheap[0] = x;  // 将堆顶元素替换为当前数AdjustDown(minheap, k, 0);  // 执行向下调整,维护最小堆性质}}for (int i = 0; i < k; i++){printf("%d ", minheap[i]);  // 输出最小堆中的前k个元素}printf("\n");free(minheap);  // 释放动态分配的堆内存fclose(fout);  // 关闭文件
}

以上就是选择排序中的几个问题,下一节排序,我们讲解的是交换排序,欢迎大家持续收看!

相关文章:

  • live555搭建流式rtsp服务器
  • 电脑文件mfc140.dll丢失的解决方法指导,怎么快速修复mfc140.dll
  • Vue2学习之第六、七章——vue-router与ElementUI组件库
  • GPS位置虚拟软件 AnyGo mac激活版
  • 机器学习 | 深入探索Numpy的高性能计算能力
  • 【LeetCode: 148. 排序链表 + 链表 + 归并排序】
  • ffmpeg 实用命令 -- 设置预览图
  • 【.NET Core】深入理解任务并行库 (TPL)
  • 使用ajax异步获取下拉列表的值
  • 单片机中MCU跑RTOS相比裸机的优势
  • 网安渗透攻击作业(1)
  • 不停机迁移,TDengine 在 3D 打印技术中的“焕新”之路
  • Linux的权限(三)
  • 数据库学习命令总结(持续更新)
  • 倍增算法笔记
  • 【剑指offer】让抽象问题具体化
  • Android 架构优化~MVP 架构改造
  • Essential Studio for ASP.NET Web Forms 2017 v2,新增自定义树形网格工具栏
  • flask接收请求并推入栈
  • flutter的key在widget list的作用以及必要性
  • Java,console输出实时的转向GUI textbox
  • JS笔记四:作用域、变量(函数)提升
  • Spark in action on Kubernetes - Playground搭建与架构浅析
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 初识 beanstalkd
  • 分享自己折腾多时的一套 vue 组件 --we-vue
  • ------- 计算机网络基础
  • UI设计初学者应该如何入门?
  • 交换综合实验一
  • ​马来语翻译中文去哪比较好?
  • ​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​
  • # centos7下FFmpeg环境部署记录
  • #免费 苹果M系芯片Macbook电脑MacOS使用Bash脚本写入(读写)NTFS硬盘教程
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (5)STL算法之复制
  • (9)目标检测_SSD的原理
  • (delphi11最新学习资料) Object Pascal 学习笔记---第7章第3节(封装和窗体)
  • (DenseNet)Densely Connected Convolutional Networks--Gao Huang
  • (附源码)小程序儿童艺术培训机构教育管理小程序 毕业设计 201740
  • (十)【Jmeter】线程(Threads(Users))之jp@gc - Stepping Thread Group (deprecated)
  • (四)JPA - JQPL 实现增删改查
  • (五)Python 垃圾回收机制
  • (一)80c52学习之旅-起始篇
  • (转)全文检索技术学习(三)——Lucene支持中文分词
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • (状压dp)uva 10817 Headmaster's Headache
  • .\OBJ\test1.axf: Error: L6230W: Ignoring --entry command. Cannot find argumen 'Reset_Handler'
  • .gitignore文件---让git自动忽略指定文件
  • .helper勒索病毒的最新威胁:如何恢复您的数据?
  • .NET Core 通过 Ef Core 操作 Mysql
  • .NET Core 网络数据采集 -- 使用AngleSharp做html解析
  • .Net Memory Profiler的使用举例
  • .NET 将多个程序集合并成单一程序集的 4+3 种方法
  • .NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉
  • .net专家(高海东的专栏)