10.2 插入排序(1)

基本思想

插入排序的基本思想在于维护一个有序区,依次在线性表中向后扫描元素,将其插入在这个有序区的合适位置。
下面讨论具体算法:设一线性表 A[1..n] 。我们令循环变量 i 2 n 。利用循环变量i便将线性表划分为两个部分 A[1..i1] A[i,n] 。在每趟循环执行前, A[1..i1] 都是局部有序的。因此在每趟循环过程中,我们的目标就是将 A[i] 插入在 A[1..i1] 的合适位置上。当然此处的插入并不能像现实中的插入,直接放进去就可以了,对于 A[k1]<A[i]<A[k] 的情形,我们需要将 A[i] 放在 A[k] 时,必须将 k i1的所有元素整体向后挪动一位。

程序实现与运行结果

因此,插入排序算法的C++语言描述如下。使用了std::vector<_Ty>泛型来接受输入,需要保证_Ty类型重载了运算符operator<(对于Java C#语言来说,类型_Ty需要继承带有比较操作的接口ComparableIComparable)。对于内层for循环来说,尤其需要注意循环的初值条件和边界条件。令ji - 1开始是因为有序区是从0i - 1的(再想一下,A[i]是当前趟需要处理的)。边界条件j >= 0而不是j > 0是因为,我们在寻找合适位置的时候,使用的是A[j] > tmp,也就是说,找到合适位置时,这个j位置对应的元素A[j]必然小于当前插入的元素。因此为了在没找到这种情况下与其保持一致,也就是A[j]是最小的,它必然会放到A[0]位置上。这时候如果j = -1就恰好保证了这种一致性。根据上面的讨论,每一趟结束时A[j + 1] = tmp而不是A[j] = tmp也就是顺理成章的了。

/**
* 插入排序。
* @param std::vector<_Ty> & list 需要排序的线性表。
*/
template<typename _Ty>
void insertion_sort(std::vector<_Ty> & A){
    //一个元素我们假定是有序的,因此没有必要从0开始循环。
    for (int i = 1; i < A.size(); i++){
        _Ty tmp = A[i];    //保存A[i]
        int j;
        //倒序挪动元素,因为有重叠且向后移动
        //需要注意的还有起始条件和终止条件。
        for (j = i - 1; j >= 0 && A[j] > tmp; j--)
            A[j + 1] = A[j];
        A[j + 1] = tmp;
        //下面的语句为了观察每一趟的输出
        for (auto it = A.begin(); it != A.end(); ++it)
            std::cout << *it << "\t";
        std::cout << std::endl;
    }
}

在主程序中设置输入为5 4 11 18 1 70 35 90 100 2,观察其每一趟执行结果如下所示,可以发现每进行一次,有序区增加一个元素而无序区减少一个元素,最终全部变成有序的。

4       5       11      18      1       70      35      90      100     2
4       5       11      18      1       70      35      90      100     2
4       5       11      18      1       70      35      90      100     2
1       4       5       11      18      70      35      90      100     2
1       4       5       11      18      70      35      90      100     2
1       4       5       11      18      35      70      90      100     2
1       4       5       11      18      35      70      90      100     2
1       4       5       11      18      35      70      90      100     2
1       2       4       5       11      18      35      70      90      100

时间与空间复杂度

首先分析一下空间复杂度。因为整个insertion_sort只使用了几个临时变量,因此很显然的,是常数空间复杂度 O(1)
分析时间复杂度的话,需要考虑到数据的分布情况。在数据完全有序的情况下,第二层循环只可能执行一次判断语句,也就是说时间复杂度只取决于外层循环的执行情况,这时候的时间复杂度为 O(n) 。相反地,如果数据完全逆序,即 A[1]>A[2]>...>A[n] ,那么每次迭代过程中,当前的 A[i] 都小于 A[1..i1] 的任意元素,因此必然需要放到 A[1] 位置。这时候内层循环需要执行到底,内层循环的执行次数为 n11i=n(n1)2 ,因此时间复杂度为 O(n2) 。这里没有采用教材的方法去分别计算移动和比较的具体次数,因为移动相对于比较来说最多多一个外层循环内部的两个赋值语句,相当于低阶项,并不影响时间复杂度。当然,时间复杂度只是一个趋势,如果赋值语句操作开销非常大,常数和低阶项带来的开销也是不能忽视的(比如对一个大的class深复制)。

稳定性

插入排序是稳定的。在每次迭代过程中,考虑内层for循环的终止条件之一A[j] <= tmp。它并不包含等号。如果出现相等的元素,就停止迭代。此时就会有 A[j]=A[j+1]=K ,但是显然 A[j+1] 的原始位置为 A[i] ,而 A[j] 是上一次迭代已经确定的,也就是说 A[j] 的原始位置来自于 A[1..i1] ,因此 A[j] 的原始位置也在 A[j+1] 的原始位置之前。因此也就保持了相对位置不变。由于上述初始情况的成立,利用数学归纳法不难得出多个相等元素的情况下,其相对位置也是保持不变的。
如果包括了等号,那就不稳定了。因为前面已经排好序的相等的元素,会被顺次向后移动,在这种情况下,可能就会出现不稳定的情况。比如序列8 7(a) 7(b) 。第一次迭代后,得到7(a) 8 7(b),第二次迭代,如果使用A[j] >= tmp进行判断,那么在j = 0时,也满足条件,最终得到7(b) 7(a) 8。显然不稳定了。

关于折半插入排序

折半插入排序,是利用二分查找,改善了查找指定位置时的时间效率,但是对于数据的移动,依然需要一个一个向后挪动,因此不能明显地改变整体的时间复杂度(元素的挪动,也就是类的深度复制可能代价远超过比较,实际上把比较的线性换成对数阶,实际改善恐怕有限),也没有太多的实际意义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值