数据结构学习笔记:排序算法
本文对常见排序算法进行了总结,如插入排序、希尔排序、桶排序、快速排序等。对于每个排序算法,给出它的概述、简单实现及复杂度分析。
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_swap 和 InsertionSort 的对比中,可以看到不使用显示交换时,算法性能的提升。另外还可以看到,随着 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. 希尔排序
未完待续……