排序(Sorting) 是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个关键字有序的序列

概念

  1. 比较排序
    在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。

  2. 非比较排序
    非比较排序是通过确定每个元素之前,应该有多少个元素来排序。

  3. 稳定排序
    通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

  4. 非稳定排序
    如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面。

  5. 原地排序
    原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。

  6. 非原地排序
    需要利用额外的数组来辅助排序。

  7. 内排序
    所有排序操作都在内存中完成。

  8. 外排序
    由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。

  • 比较排序

在冒泡排序之类的排序中,问题规模为n,又因为需要比较 n 次,所以平均时间复杂度为 O(n²) 。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为 logN 次,所以时间复杂度平均 O(nlogn) 。

比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

  • 非比较排序

针对数组 arr,计算 arr[i] 之前有多少个元素,则唯一确定了 arr[i] 在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。时间复杂度 O(n)。

非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

分类

非线性时间比较类排序

通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

排序算法 - 图2

线性时间非比较类排序

不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

排序算法 - 图3

算法复杂度

排序算法 - 图4


排序算法

冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

算法描述

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。

动图演示

排序算法 - 图5

代码实现

  1. <?php
  2. class Sorting
  3. {
  4. /**
  5. * 冒泡排序(稳定地原地排序算法)
  6. *
  7. * @param array $array
  8. * @return array
  9. */
  10. public function bubbleSort(array $array): array
  11. {
  12. if(empty($array) || count($array) < 2) {
  13. return $array;
  14. }
  15. $length = count($array);
  16. for ($i = 0; $i < $length - 1; $i++) {
  17. $done = true;
  18. for ($j = 0; $j < $length - $i - 1; $j++) {
  19. if ($array[$j] > $array[$j + 1]) {
  20. $tmp = $array[$j];
  21. $array[$j] = $array[$j + 1];
  22. $array[$j + 1] = $tmp;
  23. $done = false;
  24. }
  25. }
  26. // 没有数据交换,提前退出
  27. if ($done) {
  28. break;
  29. }
  30. }
  31. return $array;
  32. }
  33. }

算法分析

  • 最佳情况:T(n) = O(n);
  • 最差情况:T(n) = O(n2);
  • 平均情况:T(n) = O(n2);

选择排序(Selection Sort)

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法描述

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
  3. 重复第二步,直到所有元素均排序完毕。

动图演示

排序算法 - 图6

代码实现

  1. <?php
  2. class Sorting
  3. {
  4. /**
  5. * 选择排序(不稳定地原地排序算法)
  6. *
  7. * @param array $array
  8. * @return array
  9. */
  10. public function selectionSort(array $array): array
  11. {
  12. $length = count($array);
  13. for ($i = 0; $i < $length - 1; $i++) {
  14. $minIndex = $i;
  15. for ($j = $i + 1; $j < $length; $j++) {
  16. if ($array[$j] < $array[$minIndex]) {
  17. $minIndex = $j;
  18. }
  19. }
  20. $temp = $array[$i];
  21. $array[$i] = $array[$minIndex];
  22. $array[$minIndex] = $temp;
  23. }
  24. return $array;
  25. }
  26. }

算法分析

  • 时间复杂度:O(n2)
  • 空间复杂度:O(1)
  • 非稳定排序
  • 原地排序

插入排序(Insertion Sort)

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

动图演示

排序算法 - 图7

代码实现

  1. <?php
  2. class Sorting
  3. {
  4. /**
  5. * 插入排序(稳定地原地排序算法)
  6. * 时间/空间:O(n^2)/O(1)
  7. *
  8. * @param array $array
  9. * @return array
  10. */
  11. public static function insertionSort(array $array): array
  12. {
  13. if (empty($array) || count($array) == 1) {
  14. return $array;
  15. }
  16. $length = count($array);
  17. for ($i = 1; $i < $length; $i++) {
  18. $preIndex = $i - 1;
  19. $current = $array[$i];
  20. while($preIndex >= 0 && $current < $array[$preIndex]) {
  21. $array[$preIndex + 1] = $array[$preIndex];
  22. $preIndex--;
  23. }
  24. $array[$preIndex + 1] = $current;
  25. }
  26. return $array;
  27. }
  28. }

算法分析

插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

  • 时间复杂度:O(n2)
  • 空间复杂度:O(1)
  • 稳定排序
  • 原地排序

希尔排序(Shell Sort)

希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。

算法描述

我们来看下希尔排序的基本步骤,在此我们选择增量 gap=length/2,缩小增量继续以 gap = gap/2 的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  1. 选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

动图演示

排序算法 - 图8

代码实现

  1. <?php
  2. class Sorting
  3. {
  4. /**
  5. * 希尔排序(非稳定地原地排序算法)
  6. *
  7. * @param array $array
  8. * @return array
  9. */
  10. public static function shellSort(array $array): array
  11. {
  12. if (empty($array) || count($array) == 1) {
  13. return $array;
  14. }
  15. $length = count($array);
  16. // $gap = $length >> 1;
  17. $gap = 1;
  18. while ($gap < $length / 3) {
  19. $gap = $gap * 3 + 1;
  20. }
  21. for ($gap; $gap > 0; $gap = floor($gap / 3)) {
  22. for ($i = $gap; $i < $length; $i++) {
  23. $temp = $array[$i];
  24. for ($j = $i - $gap; $j >= 0 && $array[$j] > $temp; $j -= $gap) {
  25. $array[$j + $gap] = $array[$j];
  26. }
  27. $array[$j + $gap] = $temp;
  28. }
  29. }
  30. return $array;
  31. }
  32. }

算法分析

  1. 时间复杂度:O(nlogn)
  2. 空间复杂度:O(1)
  3. 非稳定排序
  4. 原地排序

归并排序(Merge Sort)

算法描述

动图演示

代码实现

算法分析


快速排序(Quick Sort)

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。


算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

动图演示

排序算法 - 图9

代码实现


算法分析

  • 最佳情况:T(n) = O(nlogn)
  • 最差情况:T(n) = O(n^2)
  • 平均情况:T(n) = O(nlogn)

堆排序(Heap Sort)

算法描述

动图演示

代码实现

算法分析


计数排序(Counting Sort)

算法描述

动图演示

代码实现

算法分析


桶排序(Bucket Sort)

算法描述

动图演示

代码实现

算法分析


基数排序(Radix Sort)

算法描述

动图演示

代码实现

算法分析