推广 热搜: 可以  搜索引擎  企业  page  个数  百度  使用  音视频  选择  父亲 

排序算法——选择排序系列及性能测试

   日期:2025-01-02     移动:http://ww.kub2b.com/mobile/quote/11770.html

初识选择排序

选择排序是一种和冒泡排序一样简单的排序算法。它的思想非常简单,假设数组大小为n,每次从数组的待排序部分中选出一个最小的元素,放到数组最左侧,n轮筛选后,数组就整体有序了。

图解如下

假设准备对如下数组进行选择排序

先对数组的待排序部分进行遍历,选出最小的元素为1,将1放到数组最左侧

第二轮,对剩下的部分进行遍历,选出最小的元素为2,将2放到最左侧(和4交换

它的代码实现也非常简单

 

优化思路

思路一

双向选择

选择排序的优化思路比较有限,根据双向冒泡的优化经验,我们可以一轮遍历选出一个最大值和一个最小值,从而提高筛选的效率。

写成代码如下

 
思路二(堆排序

堆排序

其实,堆排序也是选择排序的一个变种。堆排序需要先对数组进行建堆(大顶堆或小顶堆,然后每次将堆顶元素和堆尾元素进行交换,随后将堆尾元素在逻辑上从堆中剔除,对剩余部分的堆进行调整,再将堆顶元素和当前堆尾元素交换,以此类推。每一轮排序都会将堆顶元素追加到数组末端的有序序列中,而堆顶元素就是每一轮选出来的最大或最小值,所以堆排序也属于选择排序。

首先对堆进行一个简单的介绍。堆的本质就是一棵完全二叉树。而大顶堆,指的是堆顶元素(二叉树的根节点)要大于其左右子树的任意节点,而其余的每个非叶子节点的值,也要大于其左右子树的任意节点。下图即是一个大顶堆的逻辑结构图

可以看到堆顶元素9要大于其左右子节点8和7,节点8要大于其左右子节点4和3,7要大于其左右子节点5和6,4要大于其左右子节点1和2。这就使得堆顶元素是最大的元素。而二叉堆在实际使用中,常常以数组的形式进行存储,如下所示

紫色的数字表示元素在数组中的下标。堆从第一层开始,每一层从左往右编号,即是其在数组中存储的位置。容易得到如下的关系

若堆中的一个节点在数组中的下标为n,若其有左右子节点,则

左子节点的下标为:2n + 1

右子节点的下标为:2n + 2

若其有父节点,则父节点的下标为:(n - 1) / 2

既然堆可以用数组的形式进行存储和表示,那么就可以对数组进行建堆,然后执行堆排序。堆排序的关键在于对初始数组进行建堆,以及交换了堆顶元素和堆尾元素后的调整

图解如下

假设准备对如下数组进行堆排序

建堆的过程如下:从下往上,先找到第一个非叶子节点(没有任何子节点的节点,上图就是8,从这个节点开始,对所有非叶子节点依次进行下沉处理(向下调整)。下沉指的是,将节点与其子节点进行比较,若节点的值小于子节点中的较大者,则交换之。处理节点8,先把节点8及其子节点看成一个堆(局部堆,调整,以使这个局部堆满足大顶堆的特性。发现节点8不需要处理。

接着往前处理,到节点2,发现它比它的子节点要小,而子节点中的较大者是9,则交换2和9

接着处理节点4,发现它小于8,则交换它和8

注意下沉处理要下沉到尽可能底部的位置,4和8交换完后,继续处理4,发现4比它的子节点6要小,则交换4和6

接着处理1,发现1小于9,则交换之

继续处理1,发现1小于7,交换之

如此以来,建堆操作完成,观察可知,现在的堆已经符合大顶堆的特性

建堆完毕后,执行如下操作:将堆顶元素和堆尾元素交换,然后将堆尾元素在逻辑上从堆中删除,将剩余元素看成一个堆,对堆顶元素进行向下调整,使其成为一个大顶堆。然后又将堆顶元素和当前堆的堆尾元素交换,把堆尾元素从堆中删除,对剩余元素继续调整…如此,即完成排序。

图解如下

首先,建堆完毕后的数组状态如下

将堆顶元素和堆尾元素交换,即9和4交换

交换完毕后,将堆尾元素9在逻辑上从堆中删除。而剩余元素组成的堆,由于堆顶发生了变化,显然不满足大顶堆的性质,于是对新的堆顶进行向下调整

节点4比其子节点中的较大者8,要小,于是交换之

继续调整节点4,发现其比子节点6要小,则交换之

继续调整节点4,发现其只有一个左子节点3,且4大于3,则调整完毕(节点9已经在逻辑上从堆中移除,即堆中不再包含节点9

剩余的元素组成的堆已符合大顶堆的性质

接着,继续将堆顶元素和当前堆的堆尾元素交换,即交换8和3

然后将节点8从堆中剔除掉,对当前堆顶元素3进行向下调整,发现只需要交换3和7,调整完毕后结果为

接着交换堆顶和堆尾,即交换7和2

调整堆

交换堆顶和堆尾

调整堆

…以此类推,最终数组有序

由此可见,堆排序的关键在于建堆,以及对节点的向下调整

写成代码如下

 

注意,向下调整的过程,就是将需要调整的节点,下沉到合适的位置,考虑到先前插入排序时的优化,这里也可以采用单向赋值的方式,以避免频繁交换造成的性能开销。

性能测试

对选择排序系列的算法进行性能测试,结果如下

可见堆排序的性能完爆简单选择排序

扩展:堆排序的应用

Top K问题

从海量数据中筛选出前K个最大的数据

因为数据量特别大,对全部数据进行排序再取前K个数显然是不现实的,并且第K个以后的数是没有必要进行排序的,对K以后的数据进行排序显然浪费了性能。我们观察一下堆排序的性质,发现一个特点,无论大顶堆小顶堆,堆顶的元素总是最大的,或者最小的。如果一个大顶堆有K个元素,则堆顶的元素是这K个中最大的;如果一个小顶堆有K个元素,则堆顶的元素是这K个中最小的。那么假设一共有100个数据,要求找出前K个最大的数,可以这样想,我先取前K个元素,我只要知道当前这K个元素中最小的值,之后每次取一个新的元素,拿新元素和这个最小值比,若新元素小于这个最小值,说明新元素小于这所有的K个元素,则新元素不可能是前K个最大的数,直接抛弃之。若新元素比这个最小值大,那么把最小值剔除,换上这个新元素,再选出当前K个元素的最小值等待后续的比较。根据这个思路,只需要构建一个大小为K的小顶堆。先从海量数据中取前K个数,构建一个小顶堆。然后对K之后数,进行遍历,每次取一个数,和小顶堆的堆顶比较,若小于堆顶,则直接丢弃;若大于堆顶,则将该数和堆顶交换,并向下调整堆,维持其小顶堆的特征。因为只需要和堆顶这一个元素比较,并且小于堆顶的情况直接丢弃,只有大于堆顶的情况才需要调整堆,所以使得能够以较高的效率找到前K个最大的数,代码实现如下

 
优先级队列

队列在生活中随处可见。坐地铁时排队等车,在食堂排队打饭,去银行办理业务时也要取号排队。队列的本质就是一堆人,需要执行某个动作。最简单普通的队列就是按照先来后到的顺序依次排队,依次接收服务。但在某些场景下需要对排队的人进行一些优先级的设置,比如银行办理业务时,VIP客户的排队优先级就要高于普通客户,再比如,去食堂打饭时,如果有一个人已经10天没吃饭了,马上就要饿死了,是不是应该让他优先打饭,保证他能活过来。这些场景下,普通的队列就太死板了,无法满足需求,于是就有了优先级队列。优先级队列中的元素,不是严格按照先来后到的顺序排队的,而是优先级的高的元素,总是会排到优先级低的元素的前面,优先级相同的元素,再按先来后到的顺序排队。那么堆,这一数据结构,就能够满足这样的需求,因为它总是会把最大的,或者最小的元素,放在堆顶(数组的最前面)。

一个队列有2个基本操作:入队和出队

入队:一个人加入到队列中,进行排队

出队:轮到某个人办事了,将其移出队列

用堆的结构来表示,如下。先来一个优先级为1的人,进行排队

随后,来了一个优先级为9的人

显然不满足大顶堆的性质,则对新来的9,进行调整,此时应该是向上调整,交换1和9即可

此时又来了一个6,发现仍然满足大顶堆的特性,则不做调整

若此时,进行出队,则直接取出堆顶元素,节点9接受服务,数组变成了

对应的堆的逻辑结构就变成了

发现不满足大顶堆的特性,则进行调整,交换1和6即可

每次出队,都取数组的首元素,即堆顶元素,即能保证每次出队都是当前优先级最高的元素

其核心部分的逻辑为

  • 入队时,新加入的元素追加到数组末尾,也即堆尾,并需要对其进行向上调整,以满足大顶堆的性质。

  • 出队时,移除数组的首元素,将剩余部分元素看成一个新的堆,并重新建堆

本文地址:http://ww.kub2b.com/quote/11770.html     企库往 http://ww.kub2b.com/ ,  查看更多

特别提示:本信息由相关用户自行提供,真实性未证实,仅供参考。请谨慎采用,风险自负。


0相关评论
相关最新动态
推荐最新动态
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号