一步步地分析排序——插入排序

前言

本文是对《算法》第四版插入排序所做的笔记,与选择排序相比较(之前也写过一篇文章,简单地分析了选择排序,有兴趣的可以看看:一步步地分析排序——选择排序),虽然插入排序也是初级排序算法,排序过程也不复杂,虽然我们考虑最坏的情况时,他和选择排序以及冒泡排序同样——都是平方阶的增量。但是在某些情况下,插入排序却显得很高效。插入排序最坏时为什么是平方阶?为什么在有的时候会很高效?又是在什么时候很高效?本文都会一一记录,本文的脉络如下:

  • 概念
  • 代码
  • 时间成本
  • 空间成本
  • 输入对时间成本的影响
  • 其他特性

概念

  • 比喻:插入排序的过程类似于整理手里面的纸牌,从手中为零张纸牌起,一张张地往手里面插入纸牌,每插一张,都要保证插入之后手里的牌仍然顺序正确。
  • 具体表述:以“将数组的元素按照递增序(严格地讲应该叫做非递减序,本文后续一些表述都将以非递减序为例子)进行排列”为例,插入排序的过程是:从插入第二个元素开始,每插入一个元素,都将新元素和他前面的第一个元素比较,如果前面的第一个元素更大,则互换他们两个的位置,然后,再将新元素和他前面的第一个元素相比,循环操作,直到遇到第一个小于(不大于)新元素的那个元素,到此,完成一个元素插入。循环该过程,直到整个数组完成排序。
  • 体验过程:点击查看排序算法动态过程:
    http://visualgo.net/sorting

代码(C语言)

书本用的Java语言,而且为了表示通用性,还写成了”Comparable”的形式,我在这里为求简洁,就用C语言来描述。算法重要的是思想,不是语言层面这些东西,而且C语言写出的插入排序,和Java没有很大的区别。代码如下:

//插入排序,array:需要排序的数组,size:数组长度
void insertSort(int array[], int size)
{
    /*i记录当前排序的索引位置,j用做每个元素插入时的比较操作
    注意i是从1开始*/
    int i = 1,j;
    for(; ifor(j = i; j>0 && array[j-1]>array[j]; j--){
            exchange(array, j-1, j);
        }
    }
}

//互换数组里面索引p和q的元素
void exchange(int array[], int p, int q)
{
    int t = array[p];
    array[p] = array[q];
    array[q] = t;
}

时间成本

首先放出一个结论:插入排序时间成本受输入的影响。我们先分析每次插入的时间成本,再来分析整个算法总的时间成本,其实只要搞清楚了每次插入时的时间成本,总的成本自然就清晰了。另外按照书本约定,我们的时间成本表述为:比较次数+互换次数

每次插入时间成本

仔细想想插入排序的过程,就会发现,每插入一个新元素,有且只有以下三个情况:
1. 如果新元素和他前面的所有元素相比,是最小的,那么新元素将会和前面所有元素逐一比较,并且还要互换位置,最后插入到最左边的位置上。如图(1):
一步步地分析排序——插入排序_第1张图片
2. 如果新元素比他前面的第一个元素还大,那么新元素只会和他前面的那个元素比较一次大小,然后插入。如图(2):
一步步地分析排序——插入排序_第2张图片
3. 如果新元素比他前面的第一个元素还小,但是和他左边那些已排序的元素相比,又不会是最小的,那么新元素将会和所有比它大的元素比较,并且会互换位置,直到遇到第一个比他小的元素,和该元素进行一次比较,不会发生位置互换,比较完后直接插入。如图(3):
一步步地分析排序——插入排序_第3张图片
注意这里每个图片都只画了前面已排序的部分,后面的部分省略了。好了根据这些分析,我们得到插入一个元素时的几个情况:

  • 最慢:如果新元素比他前面所有元素都还小,满足前面所说的第一个情况。此时,比较操作和互换操作所执行的次数是一样的。我们假设在新元素前面共有m个元素,那么这次插入将会发生m次比较,m次互换。时间成本为:2m。
  • 最快:如果新元素比他前面的第一个元素还大,满足前面所说的第二个情况,那么这次插入只会发生一次比较,没有互换。所以本次插入的时间成本为:1。
  • 不是最快不是最慢:如果新元素比他前面的第一个元素还小,但是又不是前面所有的元素里最小的,满足前面所说的第三根情况,此次插入比较操作次数==互换操作+1,由于最后一次比较,新元素遇到了第一个比他小的元素,所以最后一次只有比较,没有互换。我们假设前面的元素里共有k个元素比新元素更,那么这次插入将会发生k次互换,(k+1)次比较,那么本次插入的时间成本为:2k+1。
    以上分析,得出两点:首先每插入一个新元素,只会出现三个情况。其次不论哪类情况,比较操作和互换操作的执行次数,都是有规律可循的。

算法的总时间成本

  • 最快(输入顺序):如果待排序的数组本来就已经是排好序的,那么每次插入一个元素,都将满足前面所分析的“每次插入时间成本最快情况”,所以每插入一个元素的时间成本都为:1。再者,由于插入排序是从插入第二个元素才开始操作,对于有N个元素的数组,只操作了(N-1)次,所以算法的总时间成本就为:N-1 = O(N)。
  • 最慢(输入逆序):如果待排序的数组本来是全逆序的,那么每次插入一个元素,都将满足前面所分析的“每次插入时间成本最慢情况”,所以每插入一个元素的时间成本都为:2m(注意这里每次的m都不一样)。同样,由于插入排序是从插入第二个元素才开始操作,对于有N个元素的数组,只操作了(N-1)次,所以算法的总时间成本就为:∑2m(其中m = 1,2,3…N-1) = N*(N-1) = O(N²)。
  • 平均:平均情况下,插入排序需要进行约为(N²/4)次的比较+(N²/4)次的互换,所以算法的耗时为:O(N²)。

空间成本

和选择排序相类似,除了互换元素所需要的个别额外空间,插入排序没有其他空间占用,所以插入排序是原地排序算法,空间成本不随问题规模增大而增大。

输入对时间成本的影响

前面我们已经说过,插入排序时间成本受输入数组原有的顺序影响,同时也进行了一些分析(最快,最慢)。这里我们继续介绍,并且通过数组倒置的情况,更详细地介绍算法受输入数组原有的顺序影响。

倒置的概念:

倒置指的是数组中的两个顺序颠倒的元素。比如”EXAMPLE” 里有11对倒置:E-A、X-A、X-M、X-P、X-L、X-E、M-L、M-E、P-L、P-E、L-E。——《算法》第四版(中文版)

部分有序的数组:

如果数组中倒置的数量小于数组大小的某个倍数,那么我们说这个数组是部分有序的——《算法》第四版(中文版)

另外书本所列举的几类典型的部分有序的数组:

  • 数组中每个元素距离它的最终位置都不远。
  • 一个有序的大数组接一个小数组。
  • 数组中只有几个元素的位置不正确。

插入排序时间成本与倒置的关系分析

  • 插入排序“互换操作执行次数”和“数组中元素倒置的数量”间的关系:
    由插入排序的过程可以看出,插入排序每次执行一次互换,都在“纠正”一对倒置元素。所以数组里面有多少对倒置,互换操作就执行多少次。当所有的倒置都“纠正”了,插入排序就结束了,整个数组就有序了。

  • 插入排序“比较操作执行次数”和“数组中元素倒置的数量”间的关系
    我们前面分析过每插入一个元素时的情况,得到比较的次数和互换次数之间是有规律可循,其实三类情况一共对应两类规律:对于最慢的情况,比较操作执行次数等于互换操作执行次数。剩下的两类情况,比较操作的次数等于互换操作的次数+1。基于这样的结论,再加上我们刚得出“数组里面有多少对倒置,互换操作就执行多少次。”我们现在就能得到:

    1. 比较操作执行次数不会少于(大于等于)倒置的次数。如果每次插入都满足了“最慢”情况,那么此时比较次数等于互换次数等于倒置数量。
    2. 比较操作执行次数不会多于(小于等于)倒置数量+数组长度-1。如果每次插入都满足了“最快”情况,那么此时比较次数==倒置数量+数组长度-1(这个数字其实就是互换次数+数组长度-1,就是N-1)。
  • 小结:为什么要详细分析倒置和比较操作、互换操作的关系?因为倒置在一定程度上反映“输入情况”,前面我们也约定了时间成本=比较次数+互换次数。所以这一大串的分析,可以认为是在分析输入对时间成本的影响,而且是定量分析。

其他特性

这个分析的比较少,主要两点

  • 插入排序,当前索引左边的元素,都是相对有序的,但是他们未必在最终的位置上面。和选择排序相比较,有些不同,选择排序,当前索引左边的元素,不但是相对有序的,而且它们都已经在它们最终的位置上。而插入排序,虽然这些元素相对位置都是对的,但随着新元素插入,它们可能会被移动。
  • 插入排序不会访问当前索引右侧元素,这里指的是当插入一个新的元素,而且在将它插入到正确的位置的过程里,插入排序只会访问左边那些已有序的元素,不会访问右边元素。这也是和选择排序进行对比,选择排序是在右侧选择新的元素,反而不会访问左侧元素。

你可能感兴趣的:(数据结构与算法)