堆排序与1亿数据中找出100大(小)的数据

堆排序的步骤:

例如:从小到大排序

1.由给定元素建立一棵完全二叉树

2.调整该完全二叉树使其成为最大堆

  因为堆的存储结构是数组形式,堆排序的实质就是对数组中的元素进行排序,如果按照从小到大排序的话,就说明数组最后一个元素最大,而最顶锥第一个元素和最后一个元素交换位置刚好满足;

  同理,如果要从大到小排序的话,应该建立一个最小堆

3.不断交换堆顶元素与堆未元素,进行从小到大的排序,直至输出所有堆顶元素。

一个很好的参考例子:https://www.cnblogs.com/jingmoxukong/p/4303826.html

 

内部排序与外部排序

排序大的分类可以分为两种:内排序和外排序。

在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。

一般来说外排序分为两个步骤:预处理和合并排序。首先,根据可用内存的大小,将外存上含有n个纪录的文件分成若干长度为t的子文件(或段);其次,利用内部排序的方法,对每个子文件的t个纪录进行内部排序。这些经过排序的子文件(段)通常称为顺串(run),顺串生成后即将其写入外存。这样在外存上就得到了m个顺串(m=[n/t])。最后,对这些顺串进行归并,使顺串的长度逐渐增大,直到所有的待排序的几率成为一个顺串为止。

内排序可以分为以下几类:

(1)、插入排序:直接插入排序、折半插入排序、希尔排序。

(2)、选择排序:简单选择排序、堆排序。

(3)、交换排序:冒泡排序、快速排序。

外排序可以分为一下几类(既使用内部存储也使用外部存储,内存不够时建议使用):

(4)、归并排序

(5)、基数排序

 

稳定性:就是能保证排序前两个相等的数据其在序列中的先后位置顺序与排序后它们两个先后位置顺序相同。再简单具体一点,如果A i == A j,Ai 原来在 Aj 位置前,排序后 Ai  仍然是在 Aj 位置前。

不稳定:简单选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法

稳定:冒泡排序、直接插入排序、折半插入排序,归并排序和基数排序都是稳定的排序算法。

平均时间复杂度

O(n^2):直接插入排序,简单选择排序,冒泡排序。

在数据规模较小时(9W内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为O(n^2)的算法基本上是相邻元素进行比较,基           本上都是稳定的。

O(nlogn):快速排序,归并排序,希尔排序,堆排序。

其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。

 

1亿数据中找出前k大的数据(内存不够的情境,磁盘足够大)

  当内存不够大,一般不选用将所有元素全部加载,不然超过内存限制。这里排序又分为内部排序和外部排序。

1.在内存中新建一个k的小顶锥(直接和),如果插入的元素比锥顶大,则把锥顶的元素扔掉,然后重新调整使其变成小顶锥,重复该过程,最后剩下的k个元素就是最大的。

  为什么不采用大顶锥呢,因为插入的元素如果比锥顶大的话,它就会替换锥顶,最终结果只是锥顶中的元素是最大的,无法挑选出最大的k个;

注意:堆排序实际上仅仅是利用最开始的那个元素,因为堆顶的要素要么是最大,要么是最小,其余的元素并不是有序排列的,因此,当堆得大小为k时,意味着着k个数组元素就是最大的,要从1亿个数字中挑选出前k个大数,就说明比k小的数字都比丢掉了,如何实现,就采用小顶锥的方式,每次都扔掉锥顶最小的元素,扔到最后,还剩下k个大数;

同理,当找出k个最小数字的话,意味着剩下的这k个数组元素为最小的,这就要确保每次排序要把大的都扔掉,因此要新建大顶堆。

2.采用分治法,划分为若干个小文件(通常利用hash(x)%M,m是划分的大小,来进行划分),每个文件依次找出前k大个,然后放在一起再找出k大个。每次文件找出k大个,可以利用快速排序,每次快速排序分为两部分(一边是小的,一边是大的),如果大的部分长度大于k,接着利用快速排序,直到大的部分小于k(假设为n)(说明这些肯定是这么多数中最大的),然后对剩下的部分进行快排,找出前(k-n)个最大的,然后在对剩下的进行快排,最后会发现递归到最后,只需要找到最大的那个数就行了。这种思想是分治思想,一直分下去。

如果不考虑分治思想的话,可以利用快速排序,一下子排好序,然后找出前k个最大的就行了

3.hash,采用hash主要是先去重,然后再利用分治或者是堆排序进行查找

 

补充:处理环境

实际运行:

        实际上,最优的解决方案应该是最符合实际设计需求的方案,在时间应用中,可能有足够大的内存,那么直接将数据扔到内存中一次性处理即可,也可能机器有多个核,这样可以采用多线程处理整个数据集。

       下面针对不容的应用场景,分析了适合相应应用场景的解决方案。

(1)单机+单核+足够大内存

        如果需要查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9 * 8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。这种方法简单快速,使用。然后,也可以先用HashMap求出每个词出现的频率,然后求出频率最大的10个词。

(2)单机+多核+足够大内存

        这时可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同(1)类似,最后一个线程将结果归并。

        该方法存在一个瓶颈会明显影响效率,即数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。而针对此问题,解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,知道所有数据处理完毕,最后由一个线程进行归并。

(3)单机+单核+受限内存

        这种情况下,需要将原数据文件切割成一个一个小文件,如次啊用hash(x)%M,将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,知道每个小文件小于内存大小,这样每个文件可放到内存中处理。采用(1)的方法依次处理每个小文件。

(4)多机+受限内存

        这种情况,为了合理利用多台机器的资源,可将数据分发到多台机器上,每台机器采用(3)中的策略解决本地的数据。可采用hash+socket方法进行数据分发。

 

        从实际应用的角度考虑,(1)(2)(3)(4)方案并不可行,因为在大规模数据处理环境下,作业效率并不是首要考虑的问题,算法的扩展性和容错性才是首要考虑的。算法应该具有良好的扩展性,以便数据量进一步加大(随着业务的发展,数据量加大是必然的)时,在不修改算法框架的前提下,可达到近似的线性比;算法应该具有容错性,即当前某个文件处理失败后,能自动将其交给另外一个线程继续处理,而不是从头开始处理。

        top K问题很适合采用MapReduce框架解决,用户只需编写一个Map函数和两个Reduce 函数,然后提交到Hadoop(采用Mapchain和Reducechain)上即可解决该问题。具体而言,就是首先根据数据值或者把数据hash(MD5)后的值按照范围划分到不同的机器上,最好可以让数据划分后一次读入内存,这样不同的机器负责处理不同的数值范围,实际上就是Map。得到结果后,各个机器只需拿出各自出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是Reduce过程。对于Map函数,采用Hash算法,将Hash值相同的数据交给同一个Reduce task;对于第一个Reduce函数,采用HashMap统计出每个词出现的频率,对于第二个Reduce 函数,统计所有Reduce task,输出数据中的top K即可。

        直接将数据均分到不同的机器上进行处理是无法得到正确的结果的。因为一个数据可能被均分到不同的机器上,而另一个则可能完全聚集到一个机器上,同时还可能存在具有相同数目的数据。

(这句话的意思是,MapReduce处理任务时,mapreduce在map阶段会对文件进行划分和排序,然后在reduce阶段进行归并排序。

partition是分割map每个节点的结果,按照key分别映射给不同的reduce,也是可以自定义的。这里其实可以理解归类。
我们对于错综复杂的数据归类。比如在动物园里有牛羊鸡鸭鹅,他们都是混在一起的,但是到了晚上他们就各自牛回牛棚,羊回羊圈,鸡回鸡窝。partition的作用就是把这些数据归类。只不过在写程序的时候,mapreduce使用哈希HashPartitioner帮我们归类了。)所以,数据并不是均分的,而是利用mapreduce自身的函数进行分类。

 

posted on 2018-04-06 11:47  小兔子的乌龟  阅读(1917)  评论(0编辑  收藏  举报