数据结构学习笔记:排序算法

Author Avatar
Nightn 6月 07, 2017
  • 在其它设备中阅读本文章

本文对常见排序算法进行了总结,如插入排序、希尔排序、桶排序、快速排序等。对于每个排序算法,给出它的概述、简单实现及复杂度分析。

1. 插入排序

1.1 插入排序概述

插入排序(Insertion Sort)是一种非常简单的排序方法,对于大小为 N 的数组,考虑初始状态为单独的第一个元素,之后每次插入一个元素,使得子数组总处于排序好的状态,这样,经过 N-1 次插入,整个数组将处于有序状态。下表以一个简单的例子来描述插入排序的过程。

A[0] A[1] A[2] A[3] A[4] 移动次数
原始序列 42 31 50 17 20 /
第 1 次插入 31 42 50 17 20 1
第 2 次插入 31 42 50 17 20 0
第 3 次插入 17 31 42 50 20 3
第 4 次插入 17 20 31 42 50 3

表中对一个大小 N = 5 的数组进行插入排序,初始状态认为只有第一个元素 42 处于排序好的状态,第 1 次将 31 插入子序列,由于插入后,子序列将不再处于有序状态,因此需要进行元素交换,在此交换 1 次即可;然后进行第 2 次,第 3 次插入,每次都对当前子序列进行排序(表中粗体部分表示当前已经排序好的序列),以此类推,直到第 N - 1 次(在此是第 4 次)插入,整个序列都将排好序。

1.2 插入排序实现

插入排序的实现也非常简单,在此以 C 语言为例,对大小为 N 的数组进行排序,代码如下。

void InsertionSort_swap(int A[], int N){
    for(int i = 1; i < N; i++){
        for(int j = i; j > 0 && A[j] < A[j - 1]; j--){
            int temp = A[j];
            A[j] = A[j - 1];
            A[j - 1] = temp;
        }
    }
}

外层 for 循环表示从第 1 次到 第 N-1 次插入,内层 for 循环表示对于每一次插入,都将新元素与原有元素进行比较,如果前者较小,则往前移动一位,直至达到有序状态。

上述实现虽然简单易懂,但只要仔细观察,就能发现仍有改进的余地。内层 for 循环我们通过 3 次赋值来实现相邻元素的交换,而事实上,对于新插入的元素,我们只是把当做一个比较的基准,我们不应该在每一次比较后都对这一基准进行移动,以避免显示的元素交换。因此,我们有了如下的实现。

void InsertionSort(int A[], int N){
      for(int i = 1; i < N; i++){
          int temp = A[i];
          int j;
          for(j = i; j > 0 && temp < A[j - 1]; j--){
              A[j] = A[j - 1];
          }
          A[j] = temp;
      }
}

改进之后,避免了繁琐的元素交换。在本机测试中,算法的性能也得到提升。

Algorithm InsertionSort_swap InsertionSort
N = 20000 0.410 s 0.269 s
N = 50000 2.443 s 1.570 s
N = 100000 9.675 s 6.123 s

1.3 插入排序分析

InsertionSort_swapInsertionSort 的对比中,可以看到不使用显示交换时,算法性能的提升。另外还可以看到,随着 N 的增加,算法耗时并不是线性增长的,基本上是呈现 $N^2$ 的增长趋势(如:N 从 50000 到 100000,规模变成原来的 2 倍,而耗时变成原来的 6.123 / 1.570 = 3.9 倍)。而事实上,插入排序的时间复杂度的确是 $O(N^2)$, 以下进行简单分析。

  • Worst-case

    对于 InsertionSort ,需要插入 N - 1 次,考虑最坏情况(即逆序列),第 $i$ 次插入需要进行 $i+1$ 次赋值,因此总赋值次数为:

    $$\sum_{i=2}^Ni=2+3+4+…+N=\Theta(N^2)$$

  • Best-case

    对于最优情况(即排好序的序列),每次插入都不需要移动元素,即不会进入内层 for 循环,因此其时间复杂度为 $O(N)$

  • Average-case

    而一般情况下,插入排序的平均时间复杂度为 $\Theta(N^2)$。为什么呢?在此直接引用几个结论来简单证明一下。我们注意到,对于越倾向于无序的序列,插入排序耗时越长,这二者是相关联的。那么,我们如何衡量一个序列的无序程度呢?其实就是看这个序列的逆序数的对数,逆序数的定义是:对一对数 (i, j),i < j 但是 A[i] > A[j],则称 (i, j) 是一对逆序数。

    设一个序列的逆序数是 $I$,那么用插入排序算法对这一序列排序的时间复杂度为 $O(I+N)$。那么插入排序平均时间复杂度的问题,就转变为序列的平均逆序数对数的问题。经证明,一个大小为 N 的序列,其平均逆序数为 $N\times(N-1)/4$ (证明略)。由此推出,插入排序的平均时间复杂度为 $O(N^2)$,更一般地,对于任何仅交换相邻元素的排序算法,$N^2$ 是一个下界。

因此,对于基本有序的输入,插入排序算法的执行效率较快;而倾向于无序的输入,插入排序并不令人满意。

2. 希尔排序

未完待续……