大多数纸牌玩家都用插入排序方法来给纸牌排序:他们保持手上的纸牌有序,每新抓取一张纸牌,将新纸牌插入到合适的位置。要对数组x[n]进行排序,我们从有序子数组x[0..0]开始(只包含一个元素x[0],当然有序),然后按照下面伪代码所示的方法,插入元素x[1]...x[n-1]。伪代码如下:
for i = [1, n)
/*循环不变式为:子数组x[0...i-1]是有序的*/
/*循环目标:将元素x[i]放到子数组x[0..i]中合适的位置上*/
下面展示三段代码,并展示逐次优化的过程。
代码1
insertSort1:
for i = [1, n)
//将待插入的元素与有序子数组中每个元素进行比较(如果待插入的元素大,则交换);
//直到待插入的元素找打最终的插入位置(该位置前面的元素都比它小)
for (j = i; 0 < j && x[j] < x[j - 1]; j--)
swap(j - 1, j)
上面这段代码中,swap函数的调用开销是性能的瓶颈,将该函数内联展开得到代码2
insertSort2:
for i = [1, n)
for (j = i; 0 < j && x[j] < x[j - 1]; j--)
t = x[j]
x[j] = x[j - 1]
x[j -1] = t
观察这段代码,我们发现红色标注的循环中的t被重复赋值了(在每个循环都被赋值为元素x[j]),为了节省这部分开销,我们得到代码3
insertSort3:
for i = [1, n)
t = x[j]
//通过对有序子数组中的元素进行右移操作(大于t的元素右移一位),为t空出合适的位置插入
for (j = i; 0 < j && t < x[j - 1]; j--)
//元素x[j - 1]右移一位
x[j] = x[j - 1]
//通过上面的循环,已经找到t的位置,插入
x[j] = t
结论:在元素顺序随机的情况下,insertSort2所消耗的时间是insertSort1的1/3,insertSort3所消耗的时间为insertSort2的3/4。可见性能提升无处不在。
插入排序的时间复杂度为o(n2),当数据规模增大时,插入排序的效率将难以忍受。归并排序和快速排序可以将时间复杂度提高到o(nlgn),这些是后面的课题。后面在讨论快速排序时,还会讨论到快速排序与插入排序合作的问题。