OpenDSA 完整目录

Chapter 12 Sorting

| 关于   «  2. 排序术语与记号   ::   目录   ::   4. 冒泡排序  »

3. 插入排序

3.1. 插入排序

假如你手头有过去两年的电话账单,想把它们按日期整理好,你会 怎么做? 一个相当自然的办法是:先看前两张账单,把它们排好序。 然后拿起第三张,插到与前两张相对位置正确的地方,依此类推。 每拿起一张账单,就把它放入已经整理好的那叠中。 这个简单的做法就是我们第一个排序算法——插入排序 的灵感来源。

插入排序逐个处理线性表中的记录。 每一轮处理中,把当前记录按正确的位置插入到由已处理记录构成的 已排序线性表中。 下面是一种实现。 输入是存储 \(n\) 条记录的数组 A 。

void inssort(int[] A) {
  for (int i=1; i<A.length; i++) // Insert i'th record
    for (int j=i; (j>0) && (A[j] < A[j-1]); j--)
      swap(A, j, j-1);
}
    void inssort(T[] A) {
    for (int i=1; i<A.length; i++) // Insert i'th record
        for (int j=i; (j>0) && (A[j].compareTo(A[j-1]) < 0); j--)
            swap(A, j, j-1);
}
void inssort(Comparable* A[], int n) { // Insertion Sort
  for (int i = 1; i < n; i++) // Insert i'th record
    for (int j = i; (j > 0) && (*A[j] < *A[j-1]); j--)
      swap(A, j, j-1);
}

(注意:为了让这些排序算法的解释尽可能简单,我们的可视化把 数组显示成存储简单整数的样子,而不是更复杂的记录。 但你应当明白,实践中很少会有对简单整数数组排序的需求。 我们几乎总是要排序更复杂的记录,每条记录都带有一个 键 值。 这种情况下,我们必须 有办法 把键值与记录关联起来。 上面代码的 Java 版本使用 int 数组,而 Java (Generic) 版本展示的是使用支持 Comparable 接口的 对象的实现。)

下面我们看到插入排序的前几轮处理。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

处理继续进行,每次轮到一条记录。 设当前记录为 \(x\) 。 只要它的值小于紧邻其前的记录,插入排序就会把它向左移动。 一旦遇到小于等于 \(x\) 的键值, inssort 就处理完 了这条记录,因为数组中它左侧的所有记录的键必然更小。

3.2. 插入排序分析

Settings

Proficient Saving... Error Saving
Server Error
Resubmit


Settings

Proficient Saving... Error Saving
Server Error
Resubmit


Settings

Proficient Saving... Error Saving
Server Error
Resubmit

虽然最好情况比平均情况和最坏情况快得多,但平均情况和最坏情况 通常是"典型"运行时间更可靠的指标。 不过,有些情形下我们可以预期输入是有序或近乎有序的。 一个例子是:已经排序好的线性表因少量新增元素而轻度乱序; 如果知道乱序程度很轻,用插入排序恢复有序是个好主意。 即使输入不是完全有序的,插入排序的代价也与逆序对的个数成 正比地上升。 所以对"近乎有序"的线性表,插入排序总是很快。 利用插入排序近最好情况运行时间的算法例子有 希尔排序 和 快速排序 。

统计比较次数或交换次数会得到类似的结果。 内层 for 循环每执行一轮就产生一次比较和一次交换,只有 最后一次(即让内层 for 循环测试失败的那次比较)没有 交换。 因此,整个排序过程的交换次数比比较次数少 \(n-1\) 。 最好情况下交换次数为 0,平均情况和最坏情况下为 \(\Theta(n^2)\) 。

后面我们会看到增长率远好于 \(\Theta(n^2)\) 的算法。 因此对于较大的数组,插入排序不如其他算法表现好。 所以在大多数情形下,插入排序不是最佳选择。 但也有一些特殊情形,它恰恰是理想之选。 我们已经知道,输入有序或近乎有序时插入排序表现出色。 另一个适合使用插入排序的时机是数组非常小的时候,因为插入 排序非常简单。 渐近增长率更优的算法往往更复杂,这导致其运行时间中的常数 因子更大。 这意味着对较大的数组它们通常只需要更少的比较,但每次比较的 代价更高。 这个观察似乎用处不大,因为即使是每次比较代价很高的算法,在 小规模输入上也很快。 但有时我们需要对非常小的数组做很多很多次排序。 你现在应该花点时间想一想,什么情况下需要对许多小数组排序。 实际上这种情况经常发生。

关于查找与插入的相对代价如何影响最佳排序算法的选择,参见 Computational Fairy Tales: Why Tailors Use Insertion Sort 。

   «  2. 排序术语与记号   ::   目录   ::   4. 冒泡排序  »

关闭窗口