排序算法之插入排序
前言
想象一种现实场景,几个人打扑克牌,在接牌过程中,假设按照扑克牌上的数字大小进行摆牌,假设手中已经有若干张扑克牌(按照牌面大小排好次序),那么下次接到牌之后,我们会把刚接到的牌“插入”到手中已有的牌序列中的"合适"位置,现实中的这种"接牌"思路,就是我们今天要说的"插入思想"
直接插入排序算法
给定一个长度为n的序列L,如何用"接牌"思路把它排好序呢?我们知道,排序算法涉及的两个基本操作:比较和移动元素。如果序列L是数组,我们会采用"就地排序"(in-place sort),即始终将序列L看做两部分 Sorted+Unsorted L[0,r)+L[r,n),初始化时 |S|=r=0为空序列无所谓有序。在迭代过程中,关注并处理e=L[r],在S中确定适当位置(有序序列中查找),插入e,得到有序序列L[0,r]。
算法正确性证明
不变性:随着r的递增,L[0,r)始终有序,直到r=n,L即为整体有序。
单调性:初始时,问题规模为n,即无序区间长度为n,有序区间长度为0,随着r的递增,有序序列向右扩展,无序序列区间相应缩小(减治法策略),直至最终整体有序,满足单调性
代码实现
- void insertionsort(int A[], int lo, int hi)//插入排序 A[lo,hi)
- {
- for (int i = lo + 1; i < hi; ++i)//无序序列每次取第一个元素插入有序序列中
- {
- int e = A[i]; //有序序列A[lo,i),无序序列A[i,hi),取A[i]插入有序序列合适位置
- int j = i - 1;
- for (; j >= lo; --j) //从有序序列末尾元素A[i-1]开始,逐步向前取元素与e比较
- {
- if (A[j] > e) //滚动数组,将比e大的元素向右移一个单位
- A[j + 1] = A[j];
- else //找到第一个不小于e的最大位置的元素,则结束循环
- break;
- }
- A[j+1] = e; //注意,位置j是不待遇e的最大秩,因此插入元素额要在j之后
- }
- }
分析
算法最好情况下,待排元素全部有序,这样只需比较n-1次,每次比较之后,待插入元素e仍处在原来位置,无需移动元素,时间复杂度O(n),算法最坏情况下,待排元素全部逆序,这样在插入元素时,需要比较1+2+3+4+…+n-1=,同时移动元素次数也是,这样时间复杂度为O(n2),平均复杂度也为O(n2)
由于在排序过程中,插入元素位置是满足插入依然有序的,即严格意义的大于,所以插入排序算法是稳定的排序算法。
运行结果
为了便于理解,可以将在有序向量中查找不大于元素e的位置和插入元素e这两个步骤分别编成函数,
代码实现2
- void insertionsort_B(int A[], int lo, int hi)//A[lo,hi)插入排序
- {
- for (int i=lo+1;i < hi;++i)
- {
- int e = A[i];
- int index = search(A, e, lo, i);//在A[lo,i)中查找不大于e的元素下标
- for (int j = i - 1; j > index; j--)//将A[index+1,i)元素整体向右移一个单元A(index+1,i]
- A[j + 1] = A[j];
- A[index + 1] = e;//插入元素e
- }
- }
- int search(int A[], int e, int lo, int hi) //在向量A[lo,hi)中查找不大于元素e的最大秩并返回
- {
- while (--hi >= lo)
- if (A[hi] <= e) //这样如果有多个元素与e相等,则返回最大秩
- return hi;
- return hi; //此时hi=lo-1,即有序区间越界了
- }
探索&深究
我们看到,插入排序的时间复杂度不仅与问题的规模n有关,而且与输入序列的逆序对总数I也息息相关,这种输入敏感型算法input-sensitive在排序算法中有着特殊地位。从上面的有序向量查找过程中,我们可以看到,insert(search(A,e)+1,e)这条语句仍然会保持有序向量A的有序性,这得益于我们的search每次返回的都是不大于e的最大秩,即使有序向量中有多个重复元素e,仍然会返回e的最大秩,这样才保证插入不破坏有序性。
创新性思维
既然向量A是有序的,那么我们采用二分法查找不是更能快速得到结果吗?二分查找使得查找成功的平均时间性能为O(logn)比起顺序查找的O(n)有很大提高,下一篇我将着手探讨有序序列的二分查找奥秘!