算法描述
插入排序的过程和平时打牌的时候给手里的牌排序差不多:
- 从牌桌上抽一张牌
- 把抽到的牌从右到左(或者从左到右)挨个和手里的牌进行比较,当发现左边的牌大一些,右边的牌小一些,就将牌插入到该位置
- 重复执行步骤1,直到牌抽完了
参考动画:visualgo.net
代码实现(C++)
#include <algorithm>
#include <utility>
/**
* 对范围[first, last)的元素进行插入排序
* @param first 双向迭代器开头
* @param last 双向迭代器结尾
* @param comp 比较函数
*/
template<typename BidirectionalIterator, typename Compare>
void insertionSort(BidirectionalIterator first, BidirectionalIterator last, Compare comp) {
if (first != last) {
// [first, sortedEnd)表示目前已经排好序的范围
for (auto sortedEnd = first; ++sortedEnd != last;) {
if (comp(*sortedEnd, *first)) {
// 当前元素val应该排在第一位
auto val = std::move(*sortedEnd);
auto next = sortedEnd;
std::move_backward(first, sortedEnd, ++next);
*first = std::move(val);
} else {
// prev表示当前元素val所在正确位置的前一个位置
auto prev = sortedEnd;
--prev;
auto next = sortedEnd;
auto val = std::move(*sortedEnd);
while (comp(val, *prev)) {
*next = std::move(*prev);
next = prev;
--prev;
}
*next = std::move(val);
}
}
}
}
算法复杂度
对于排序算法而言,它并不会关心自己处理的数据具体代表什么,而只关心数据之间的大小关系。所以对于任意一组数据,都可以将里面的元素抽象成其在整组数据里面的大小排名(rank)。简单起见,这里假设所有元素大小均不相同,于是任一组数据便可对应于集合(pi={1,2,3,cdots,n})的一种排列。
然后引入逆序对(invertion pair)的概念:
定义:在数列(A[1ldots n])中若存在下标(1leq i<jleq n)使得 A[i]>A[j] ,则称((A[i],A[j]))为一个逆序对。数列中逆序对的总数为逆序数
显然数组的逆序数能很好地描述该数组的无序程度。而且不难看出的是一组数据的逆序数刚好就是插入排序需要执行的比较移动操纵的次数。因为在进行元素的插入时,每一次比较出元素该再往前一步时,随着较大的元素后移一步恰好就修复了一个逆序对。
- 最好情况:当数组本身有序时,逆序数为0,时间复杂度为(Theta(n))
- 最坏情况:当数组本身逆序时,逆序数为(sum^{n}_{i=1}{(i-1)}=frac{n(n-1)}{2}),时间复杂度为(Theta(n^2))
- 平均情况:当数组随机排列时,令随机变量(X)表示逆序数,那么期望值(E(X)=frac{n(n-1)}{4}),时间复杂度为(Theta(n^2))
期望值(E(X))求解过程:
令随机变量
[X_{ij}= egin{cases} 1, & ext {if (A[i],A[j]) is an inversion pair} \ 0, & ext{otherwise} end{cases}
]
则
[X=sum^{}_{1leq i < j leq n}{X_{ij}}
]
[E(X)=sum^{}_{1leq i < j leq n}{E(X_{ij})}
]
很明显((A[i],A[j]))是逆序对的概率是(frac{1}{2}),所以期望也是(frac{1}{2})
[E(X)=sum^{}_{1leq i < j leq n}{frac{1}{2}}=frac{n(n-1)}{4}
]
总结
- 对于近乎有序的数据而言插入排序算法十分高效。
- 对于规模很小的数据插入排序也是极佳的选择,由于其所进行的操作很简单所以平均每次操作的开销都很小。这一点使得像快速排序以及归并排序等算法都用插入排序来处理自己划分出来的小区间以优化性能。
- 插入排序是稳定的排序算法,即不改变相同大小元素的原始次序关系。