技术分享之八大排序算法(均已以升序为例)

一、排序名称

内部排序:指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。其中快速排序的是目前排序方法中被认为是最好的方法。

1、插入排序:直接插入排序、(shell)希尔排序

2、交换排序:冒泡排序、快速排序

3、选择排序:简单选择排序、堆排序

4、归并排序

5、基数排序

 

外部排序:指的是大文件的排序,即待排序的记录存储在外存储器(硬盘…)上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。

例如:将原文件分解成多个能够一次性装入内存的部分分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序。内存才2G,外存有1000G待排序文件 = 500个 x 2G,然后每2G转入内存进行排序,排完序写入外存, 最后将500个排好序的文件进行归并排序。

 

二、性能元素

1、稳定性 

(1)通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。例如:{ 2 5 3 7 10 7 9} —>  { 2 3 5 7 7 9 10} ,一次排序后,红色的"7"还是在绿色的"7"前面。

(2)稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序在高位也相同时是不会改变的。

     

2、时间复杂度 T(n)=O(f(n))

(1)算法的时间复杂度O(n)是一个问题规模n函数,没有严格的定义,它定量描述了该算法的运行所需要的时间。衡量一个算法的效率,如果以每条代码的实际执行次数,虽然精确,但十分烦琐。因此人们设计了用数量级的方法来衡量算法效率,如甲程序的执行次数为2n(n为数据量个数),乙为 3n+2,则当 n 很大时,认为甲乙是等数量级的,是等效率的

(2)时间频度不同(一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)),但时间复杂度可能相同。如:T(n)=n^2+3n与T(n)=n^2+4n它们的频度不同,但时间复杂度相同,都为O(n^2)。

(3)时间复杂度比较简单的计算方法是:看看有几重for循环,没有循环时间复杂度为O(1),只有一重则时间复杂度为O(n),二重则为O(n^2),依此类推,如果有二分则为O(logn),二分例如快速幂、二分查找,如果一个for循环套一个二分,那么时间复杂度则为O(nlogn)。

 

3、空间复杂度 S(n)=O(f(n))

(1)它也是问题规模n的函数,S(n)定义为一个算法在运行过程中临时占用辅助存储空间大小的量度。

(2)算法的空间复杂度一般也以数量级的形式给出。

   当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);

   当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n);

   当一个算法的空间复杂度与n成线性比例关系时,可表示为O(n)。

 

三、性能总结

 

 

四、分别介绍

1、简单选择排序

思想:

   一句话概括就是依次按位置挑选出适合此位置的元素来填充。

过程:

(1)暂定第一个元素为最小元素,往后遍历,逐个与最小元素比较,若发现更小者,与先前的"最小元素"交换位置。达到更新最小元素的目的。

(2)一趟遍历完成后,能确保刚刚完成的这一趟遍历中,最的小元素已经放置在前方了。然后缩小排序范围,新一趟排序从数组的第二个元素开始。

(3)在新一轮排序中重复第1、2步骤,直到范围不能缩小为止,排序完成。

示例:

以下为选择排序的存储状态,其中大括号内为无序区,大括号外为有序序列:

初始序列:{49 27 65 97 76 12 38}

  第1趟:12与49索引交换:12 {27 65 97 76 49 38}

  第2趟:27不动 :            12 27 {65 97 76 49 38}

  第3趟:65与38索引交换:12 27 38  {97 76 49 65}

  第4趟:97与49索引交换:12 27 38 49   {76 97 65}

  第5趟:76与65索引交换:12 27 38 49 65   {97 76}

  第6趟:97与76索引交换:12 27 38 49 65 76 97 完成

核心代码:

 

2、冒泡排序

思想:

    一句话概括就是将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,浮在上面,较大的元素往下沉。

过程:

(1)在一趟遍历中,不断地对相邻的两个元素进行排序,小的在前大的在后,这样会造成大值不断沉底的效果,当一趟遍历完成时,最大的元素会被排在后方正确的位置上。

(2)然后缩小排序范围,即去掉最后方位置正确的元素,对前方数组进行新一轮遍历,重复第1步骤。直到范围不能缩小为止,排序完成。

示例:

以下为选择排序的存储状态,其中大括号内为无序区,大括号外为有序序列:

初始序列:{49 27 65 97 76 12 38}

  第1趟:97沉底:{ 49 27 65 76 12 38} 97

  第2趟:76沉底:{ 49 27 65 12 38 } 76 97

  第3趟:65沉底:{ 49 27 12 38 } 65 76 97

  第4趟:49沉底{ 27 12 38 } 49 65 76 97

  第5趟:38沉底:{ 27 12 } 38 49 65 76 97

  第6趟:27沉底:{ 12 } 27 38 49 65 76 97

      第7趟:12沉底:12  27 38 49 65 76 97 完成 

核心代码:

 

3、直接插入排序

思想:

插入排序是从一个乱序的数组中依次取值,插入到一个已经排好序的数组中,比较的两个记录跨度为1。可以想象一下打扑克牌插牌时的情景。这看起来好像要两个数组才能完成,但如果只想在同一个数组内排序,也是可以的。此时需要想象出两个区域:前方有序区和后方乱序区。

某些情况下效率很高:1、记录本身基本有序   2、记录数比较少

过程:

(1)分区。开始时前方有序区只有一个元素,就是数组的第一个元素。然后把从第二个元素开始直到结尾的数组作为乱序区。

(2)从乱序区取第一个元素,把它正确插入到前方有序区中。把它与前方有序区的最后一个元素比较,亦即与它的前一个元素比较。

    • 如果比前一个元素要大,则不需要交换,这时有序区扩充一格,乱序区往后缩减一格,相当于直接拼在有序区末尾。
    • 如果和前一个元素相等,则继续和前二元素比较、再和前三元素比较......如果往前遍历到头了,发现前方所有元素值都一样,那也可以不需要交换,这时有序区扩充一格,乱序区往后缩减一格,相当于直接拼在有序区末尾。
    • 如果比前一个元素小,则交换它们的位置。交换完后,继续比较取出元素和它此时的前一个元素,若更小就交换,若相等就比较前一个,直到遍历完成。
    • 至此,把乱序区第一个元素正确插入到前方有序区中。

(3)往后缩小乱序区范围,继续取缩小范围后的第一个元素,重复第2步骤。直到范围不能缩小为止,排序完成。

示例:

以下为选择排序的存储状态,其中大括号内为无序区,大括号外为有序序列:

初始序列:{49 27 65 97 76 12 38}

  第1趟:无序区的27添加到有序区49的前面 {27 49} { 65 97 76 12 38}

  第2趟:无序区的65添加到有序区49的后面 {27 49 65} { 97 76 12 38}

  第3趟:无序区的97添加到有序区65的后面 {27 49 65 97} { 76 12 38}

  第4趟:无序区的65添加到有序区97的前面,49的后面 {27 49 65 76 97} { 12 38}

  第5趟:无序区的76添加到有序区97的前面,65的后面 {12 27 49 65 76 97} { 38}

  第6趟:无序区的12添加到到有序区27的前面 {12 27 49 65 76 97} {38}

      第7趟:无序区的38添加到到有序区49的前面,27的后面  {12 27 38 49 65 76 97} 完成

核心代码:

 

4、快速排序

思想:

通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

过程:

 

示例:

上图中,演示了快速排序的处理过程:

初始状态为一组无序的数组:2、4、5、1、3。

经过以上操作步骤后,完成了第一次的排序,得到新的数组:1、2、5、4、3。

新的数组中,以2为分割点,左边都是比2小的数,右边都是比2大的数。

因为2已经在数组中找到了合适的位置,所以不用再动。

2左边的数组只有一个元素1,所以显然不用再排序,位置也被确定。(注:这种情况时,left指针和right指针显然是重合的。因此在代码中,我们可以通过设置判定条件left必须小于right,如果不满足,则不用排序了)。

而对于2右边的数组5、4、3,设置left指向5,right指向3,开始继续重复图中的一、二、三、四步骤,对新的数组进行排序

核心代码:

 

5、堆排序

堆:堆是一棵顺序存储的完全二叉树。

其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。

其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。

举例来说,对于n个元素的序列{R0, R1, ... , Rn}当且仅当满足下列关系之一时,称之为堆:

(1) Ri <= R2i+1  Ri <= R2i+2 (小根堆)

(2) Ri >= R2i+1  Ri >= R2i+2 (大根堆)

其中i=1,2,…,n/2向下取整; 

 

思想(最大堆进行升序排序):

① 初始化堆:将数列a[1...n]构造成最大堆。

② 交换数据:将a[1]和a[n]交换,使a[n]是a[1...n]中的最大值;然后将a[1...n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1...n-1]中的最大值;然后将a[1...n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。整个过程就是:创建堆—堆排序—创建堆—堆排序......堆排序(Heap Sort)只需要一个记录元素大小的辅助空间(供交换用),每个待排序的记录仅占有一个存储空间。

过程:

(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大),按层倒着选择非叶节点去比较。

(2)每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。 当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。

示例:

(1)构建堆

   无序序列{ 1 , 3, 4, 5, 2, 6, 9,  7,  8,  0}

 

  (2)堆排序

 

核心代码:

 

6、希尔排序

思想:

又称为缩小增量排序,它也是一种插入排序。不过它是直接插入排序算法的一种威力加强版。把记录按步长 gap 分组,对每组记录采用直接插入排序方法进行排序。随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。 

过程:

初始时,有一个大小为 10 的无序序列。

在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。

接下来,按照直接插入排序的方法对每个组进行排序。

在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。

按照直接插入排序的方法对每个组进行排序。

在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。

按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。

需要注意一下的是,图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到,在排序过程中,两个元素位置交换了。

示例:

核心代码:

 

7、归并排序

理解:

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

思想:

将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。

归并排序其实要做两件事:

(1)“分解”——将序列每次折半划分。

(2)“合并”——将划分后的序列段两两合并后排序。

过程:

1、先考虑第二步,如何合并?

在每次合并过程中,都是对两个有序的序列段进行合并,然后排序。

这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。

先将他们合并到一个局部的暂存数组R2中,待合并完成后再将R2复制回R中。

为了方便描述,我们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。

每次从两个段中取出一个记录进行关键字的比较,将较小者放入R2中。最后将各段中余下的部分直接复制到R2中。

经过这样的过程,R2已经是一个有序的序列,再将其复制回R中,一次合并排序就完成了。

2、其次,掌握了合并的方法,接下来,让我们来了解如何分解?其实就是折半划分,直到每一个有序序列长度为1结束。

示例:

 

核心代码:

8、基数排序

思想:

基数排序与本系列前面讲解的七种排序方法都不同,它不需要比较关键字的大小。它是根据关键字中各位的值,通过对排序的N个元素进行若干趟“分配”与“收集”来实现排序的。

过程:

通过一个具体的实例来展示一下,基数排序是如何进行的。 

设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。

我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以0~9来表示的。

所以我们不妨把0~9视为10个桶。 

我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是0,将这个数存入编号为0的桶中。

分类后,我们在从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。

这时,得到的序列就是个位数上呈递增趋势的序列。 

按照个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。

接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。位数不够的,在数字前面补充0后,再填桶。 

示例:

{50, 123, 543, 187, 49, 30, 0, 2, 11, 100}

50 30 0 100 11 2 123 543 187 49  个位

0 100 2 11 123 30 543 49 50 187  十位

0 2 11 30 49 50 100 123 187 543  百位

核心代码:

 

 

五、参考地址

稳定性:http://www.cnblogs.com/codingmylife/archive/2012/10/21/2732980.html

iOS演示:http://www.jianshu.com/p/70619984fbc6?utm_campaign=hugo&utm_medium=reader_share&utm_content=note&utm_source=weixin-friends

Java排序总结:http://www.jianshu.com/p/7d037c332a9d?utm_campaign=hugo&utm_medium=reader_share&utm_content=note&utm_source=weixin-friends

堆排序:http://www.cnblogs.com/skywang12345/p/3602162.html

posted @ 2017-05-29 12:03  XYQ全哥  阅读(1015)  评论(0编辑  收藏  举报