排序算法

基本概念

  • 排序算法的稳定性:如果待排序的表中,存在多个关键字相同的元素,经过排序后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序算法是稳定的,反之则为不稳定。
  • 内排序:排序过程中,整个表都是放在内存中处理,排序时不涉及数据的内、外交换
  • 外排序:指能够处理极大量数据的排序算法,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上,外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。

内排序算法

  • 插入排序
  • 交换排序
  • 选择排序
  • 归并排序
  • 基数排序

插入类排序

  • 直接插入排序:
    屏幕快照 2016-08-12 下午10.16.25.png
    现有一组数组 \(arr = [5, 6, 3, 1, 8, 7, 2, 4]\),共有8个记录,排序过程如下:
    屏幕快照 2016-08-17 下午4.36.05.png

    • 最好情况下「正序」的时间复杂度:\(O(n)\),比较次数为\(n-1\)次,移动次数\(2(n-1)\)次

    • 最坏情况「逆序」下的时间复杂度:\(O(n^2)\),比较次数\(\sum_{i=1}^{n-1}i\),移动次数\(\sum_{i=1}^{n-1}(i+2)\)

    • 平均时间复杂度:\(O(n^2)\)

    • 空间复杂度:\(O(1)\)

    • 不能保证一趟之后有一个元素在其最终的位置

    • 是稳定的排序算法

  • 折半插入排序:

    屏幕快照 2016-08-12 下午10.27.32.png

    • 注意后面的第二个for循环中 \(j >= high + 1\),而不是\( j >= high \)

    • 从时间上看,折半插入排序只是减少了关键字间的比较次数,而元素的移动次数不变,因为找到位置之后还是要将元素全部移动,因此平均时间复杂度:\(O(n^2)\)

    • 空间复杂度:\(O(1)\)

  • \(Shell\)排序: 又称为缩小增量排序方法,其基本思想是:把记录按下标的某个增量\(d\)分组,对每组记录采用直接插入排序方法进行排序,随着增量逐渐缩小,所分成的组所包含的记录越来越多,到增量的值减少到\(1\)时,整个数据合成为一组,构成一组有序记录,则完成排序。

    • 先取一个正整数 \(d_1(d_1 < n)\),把全部记录分成 \(d_1\) 个组,所有距离为 \(d_1\) 的倍数的记录看成一组,然后在各组内进行插入排序
    • 然后取 \(d_2(d_2 < d_1)\)重复上述分组和排序操作;直到取 \(d_i = 1(i >= 1)\) 位置,即所有记录成为一个组,最后对这个组进行插入排序。一般选 \(d_1 约为 n/2,d_2 为 d_1 /2, d_3 为 d_2/2 ,…, d_i = 1\)。
      屏幕快照 2017-05-14 上午9.23.48
      屏幕快照 2016-08-29 下午10.18.54.png
      屏幕快照 2016-08-17 下午4.00.17.png
      屏幕快照 2016-08-17 下午4.01.46.png

    • \(shell\)排序的每趟排序,都会使得整个序列变得更加有序,等整个序列基本有序了,再来一趟直接插入排序,这样会使排序效率更高

    • 不能保证一趟之后有一个元素在其最终的位置

    • \(shell\)排序是 不稳定的排序算法,例如\(2,2,1(d=2,1)\)

    • 分析\(shell\)排序是一个复杂的问题, 它的时间复杂度是"增量“序列的函数,到现在为止还未得到数学上的解决

      屏幕快照 2016-08-17 下午4.28.49.png

    • 空间复杂度为\(O(1)\)


交换类排序

  • 大数沉底的冒泡排序的基本流程

    • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

    • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。交换到最后的时候,最后的一个元素将会是最大的数

    • 针对所有的元素重复以上的步骤,除了刚刚找到的最后一个

    • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
      屏幕快照 2016-08-17 下午5.09.08.png
      屏幕快照 2016-08-17 下午4.58.11.png
      屏幕快照 2016-08-18 上午8.56.53.png

    • 最坏情况:待排序序列逆序,时间复杂度为\(O(n^2)\)

    • 最好情况:待排序序列有序,时间复杂度为\(O(n)\)

    • 平均时间复杂度为:\(O(n^2)\)

    • 空间复杂度为\(O(1)\)

    • 每次都能保证一个元素在最终位置

    • 是稳定的排序算法

  • 快速排序:它采用了一种分治的策略,将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。

    • 在数据集之中,选择一个元素作为"基准"\((pivot)\)

    • 所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边

    • 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止
      假设数据集为\({85, 24, 63, 45, 17, 31, 96, 50}\),怎么对其排序呢
      屏幕快照 2016-08-17 下午5.31.24.png
      屏幕快照 2016-08-17 下午5.32.05.png
      屏幕快照 2016-08-17 下午7.44.23.png
      屏幕快照 2016-08-27 下午8.34.35.png
      屏幕快照 2016-08-17 下午8.14.39.png
      屏幕快照 2016-08-27 下午8.10.01.png
      屏幕快照 2017-05-02 下午12.01.53
      上面这种方法应该注意后面是small++之后再交换,想象一下全部有序就能理解了😂

    • 快速排序最好情况下的时间复杂度为\(O(nlog_2n)\)

    • 最坏情况下的时间复杂度为\(O(n^2)\)

    • 平均时间复杂度为\(O(nlog_2n)\),就平均而言快速排序是所有排序算法中效果最好的, 这是快排的名字的由来

    • 待排序序列越接近无序,算法效率越高,个人理解应该是无序的时候基点两边平衡的比较好,所以效率比较高。

    • 待排序序列越接近有序,算法效率越低(快速排序反而蜕化为冒泡排序?)

    • 快速排序的趟数和初始序列相关

    • 不稳定的排序算法\(2,2,1\)

    • 算法的空间复杂度为\(O(log_2n)\)


选择类排序

  • 简单选择排序:每一趟在\(n-i(i=1,2,...,n-1)\)个数据元素中\(R[i],R[i+1],...,R[n-1]\)中选择最小的数据元素作为有序序列中第\(i\)个数据元素。
    屏幕快照 2017-05-02 下午10.35.42
    屏幕快照 2016-08-17 下午9.51.44.png

    • 不稳定的排序算法:\(2,2,1\)

    • 时间复杂度为\(O(n^2)\)

    • 空间复杂度为\(O(1)\)

  • 堆排序:可以把堆 看成一棵完全二叉树,满足:任何一个非叶子节点的值都不大于(或不小于)其左右孩子节点的值,若父亲大孩子小,则这样的堆叫做大顶堆,若父亲小孩子大,则这样的堆叫做小顶堆。

    根据堆的定义,代表堆的这棵完全二叉树的根节点的值是最大的或者最小的,因此将一个无序的序列调整为一个堆,就可以找出这个序列的最大(最小)值,然后将找出的这个值交换到序列的最后(或最前),这样有序序列元素增加1个,无序序列中元素减少1个,对新的无序序列重复这样的操作就可以实现排序。

屏幕快照 2016-08-17 下午10.37.22.png
屏幕快照 2016-08-17 下午10.29.11.png

在判断R[i]<R[i+1]的时候,别忘记了要判断i< high

屏幕快照 2016-08-18 上午9.32.37.png
屏幕快照 2016-08-17 下午10.38.51.png
屏幕快照 2016-08-17 下午10.41.38.png

  • 平均时间复杂度为\(O(nlog_2(n))\)

  • 最坏情况下的时间复杂度也为\(O(nlog_2(n))\)

  • 空间复杂度为\(O(1)\), 空间复杂度是指占用内存大小,每次调用完调整堆的的函数后,所占用的那一个空间都会被释放,不想快速排序那样未排完之前是不会释放辅助空间的

  • 适用于记录数较多的情况

  • 是不稳定排序方法\(2,2,1\)(小顶堆)


归并排序

  • \(2\)路归并排序:将两个有序的子序列合并成一个新的有序子序列。归并的思想:将序列看成\(n\)个有序子序列,将序列看成是\(n\)个有序子序列,每个序列的长度为\(1\),然后两两归并,得到\(⌈\frac{n}{2}⌉\)个长度为\(2\),的有序子序列,然后两两归并....,如此重复,直到得到一个长度为\(n\)的有序子序列
    屏幕快照 2016-08-18 上午10.09.34.png
    屏幕快照 2016-08-18 上午10.23.57.png
    屏幕快照 2016-08-18 上午10.43.29.png
    屏幕快照 2016-08-18 上午10.43.43.png

    • 排序时间代价不依赖于待排序数组的初始情况

    • 最好,最坏,平均时间复杂度都为\(O(nlog_2n)\)

    • 空间复杂度为\(O(n)\)

    • 稳定的排序方法

    • 在平均情况下还是快速排序最快(常数因子更小)


基数排序

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。基数排序法会使用到桶 \((Bucket),先进先出的队列\),顾名思义,通过将要比较的位(个位、十位、百位…),将要排序的元素分配至 \(0 \to 9\) 个桶中,借以达到排序的作用,在某些时候,基数排序法的效率高于其它的比较性排序法。

屏幕快照 2016-08-18 上午10.53.24.png

  • 是稳定的排序方法

  • 时间复杂度:设数组长度为\(n\),基数为\(r\),关键字位数为\(d\),则每趟分配的时间为\(O(n)\),每趟收集的时间复杂度为\(O(n)\),工序\(d\)趟分配与收集,所以时间复杂度为\(O(d(2n))\),即\(O(d \times n)\)

  • 空间复杂度为\(O(rn)\)

屏幕快照 2016-08-27 下午8.47.03.png


外部排序

所谓外排序,顾名思义,即是在内存外面的排序,因为当要处理的数据量很大,而不能一次装入内存时,此时只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。

  • 在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件;
  • 尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
  • 例子:假定现在有\(20\)个数据的文件\(A:{5,11, 0, 18, 4, 14, 9, 7 6, 8, 12, 17 ,16 ,13 ,19 ,10, 2, 1, 3, 15}\),但一次只能使用仅装\(4\)个数据的内容,所以,我们可以每趟
    对\(4\)个数据进行排序,即\(5\)路归并,具体方法如下述步骤:

    • 我们先把“大”文件\(A\),分割为\(a_1,a_2,a_3,a_4,a_5等5\)个小文件,每个小文件\(4\)个数据
    • \(a_1文件为:5,11, 0,18\)
    • \(a_2文件为:4,14,9,7\)
    • \(a_3文件为:6, 8, 12, 17\)
    • \(a_4文件为:16,13, 19,10\)
    • \(a_5文件为:2,1,3,15\)然后依次对\(5\)个小文件分别进行排序
    • \(a_1文件完成排序后:0,5,11,18\)
    • \(a_2文件完成排序后:4,7,9,14\)
    • \(a_3文件完成排序后:6,8,12,17\)
    • \(a_4文件完成排序后:10,13,16,19\)
    • \(a_5文件完成排序后:1,2,3,15\)最终多路归并,完成整个排序 整个大文件\(A\)文件完成排序后:\(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19\)
  • 例子:要对\(900MB\)的数据进行排序,但机器上只有\(100MB\)的可用内存时,外部归并排序按如下方法操作:

    • ①读入\(100MB\)的数据至内存中,用某种常规排序(如:快速排序、堆排序、归并排序等)在内存中完成排序。
    • ②将排序完成的数据写入磁盘(临时文件)。
    • ③重复步骤\(1\)和步骤\(2\)直到所有的数据都存入了不同的\(100MB\)的块(临时文件)中。本例中,\(900MB数据,100MB\)内存,故产生了9个临时文件。
    • ④读入每个临时文件(顺串)的前\(10MB(=100MB/(9块+1)\),在进行多路归并的时候,这里有\(9\)路,然后输出缓冲算一个,所以要将内存资源分给\(10\)个部分)的数据放入内存中的输入缓冲区(总计\(90MB\)),最后的\(10MB\)作为输出缓冲区。(实践中,将输入缓冲适当调小,而适当增大输出缓冲区能获得比较好的效果)
    • ⑤执行\(9\)路归并算法,将结果输出到输出缓冲区。一旦输出缓冲区满,将缓冲区中的数据写到目标文件,清空缓冲区。一旦\(9\)个输入缓冲区的一个变空,就从这个缓冲区关联的文件中读入下一个\(10MB\)数据,除非这个文件已读完。这是“外部归并排序”能在主存外完成排序的关键步骤——因为“归并算法”对每一个大块只是顺序地做一轮访问(进行归并),每个大块不用完全载入主存。
    • \(1-3\)步是排序,\(4-5\)步是归并。
    • 为了增加每一个有序的临时文件的长度,可以采用置换选择排序。它可以产生大于内存的顺串。具体方法是在内存中使用一个最小堆进行排序,设这个最小堆的大小为\(M\),算法描述如下:
    • ①初始时将输入文件读入内存,建立最小堆。
    • ②将堆顶元素输出至输出缓冲区。然后读入下一个记录。
      • 2.1若该元素的关键码值不小于刚输出的关键码值,将其作为堆顶元素并调整堆,使之满足堆的性质。
      • 2.2若该元素的关键码值小于刚输出的关键码值,将新元素放入堆底位置,将堆的大小减\(1\)。
    • ③重复第②步,直至堆大小变为\(0\)。
    • ④此时一个顺串已经产生。将堆中的所有元素建堆,开始生成下一个顺串。

屏幕快照 2016-08-27 下午9.32.02.png
屏幕快照 2016-08-28 上午8.28.38.png
屏幕快照 2016-09-08 下午8.19.57

屏幕快照 2016-09-08 下午8.20.26

桶排序

桶排序\((Bucket \ Sort)\)的基本思路是:

  • 将待排序元素划分到不同的桶。先扫描一遍序列求出最大值\(max\)和最小值\(min\),设桶的个数为\(k\),则把区间\([min, max]\)均匀划分成\(k\)个区间,每个区间就是一个桶。将序列中的元素分配到各自的桶。

  • 对每个桶内的元素进行排序。可以选择任意一种排序算法。

  • 将各个桶中的元素合并成一个大的有序序列。

  • 假设数据是均匀分布的,则每个桶的元素平均个数为\(\frac{n}{k}\)。

  • 假设选择用快速排序对每个桶内的元素进行排序,那么每次排序的时间复杂度为\(O(\frac{n}{k}log(\frac{n}{k}))\)。

  • 总的时间复杂度为\(O(n)+O(m)O(\frac{n}{k}log(\frac{n}{k}))=O(n+nlog(\frac{n}{k}))=O(n+nlogn-nlogk)\)。当 \(k\) 接近于 \(n\) 时,桶排序的时间复杂度就可以近似认为是 \(O(n)\) 的。即桶越多,时间效率就越高,而桶越多,空间就越大。


计数排序

计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。当输入的元素是n个0到k之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存

a1
屏幕快照 2017-05-14 上午11.44.22


posted @ 2017-04-17 22:46  I呆呆  阅读(528)  评论(0编辑  收藏  举报