排序算法是算法导论一书中首先介绍的内容,由它引出了如何证明算法是正确的(循环不变式),也引出了算法复杂度的计算。
对插入排序而言,它的工作方式就像我们在打牌时排序一手扑克牌一样。我们每次摸到一张牌后都会把这张牌与我们手里的牌进行对比,然后将它放在正确的位置。
我们以这样一组数{31,41,59,26,41,58}为例(算法导论书中的课后题 ),来说明插入排序具体的执行过程。
C++代码:
#include<iostream>
using namespace std;
void InsertionSort(int* arr, int len) {
for (int i = 1; i < len; i++) {
int temp = arr[i];
for (int j = i - 1; j >= 0&& temp < arr[j]; j--) {
arr[j + 1] = arr[j];
arr[j] = temp;
for (int m = 0; m < len; m++) {
cout << arr[m] << ",";
}cout << endl;
}
}
}
void InsertionSortTE(int* arr, int len) {
for (int i = 1; i < len; i++) {
int temp = arr[i];
int j = i - 1;
for (; j >= 0 && temp < arr[j]; j--) {
arr[j + 1] = arr[j];
}
arr[j+1] = temp;
}
}
int main() {
int arr[6] = { 31,41,59,26,41,58 };
//InsertionSort(arr, 5);
InsertionSortTE(arr, 6);
for (int m = 0; m < 6; m++) {
cout << arr[m] << ",";
}
}
主体函数是InsrectionSortTE,函数InsertionSort是为了说明其每一步的过程有怎样的输出,其输出如下
也可以看出其操作原理。
接下来我们来分析一下代码,并证明该代码确实可以实现对一组数的非降序排序。
要想证明该算法的正确性,我们需要引入循环不变式的概念:1.初始化:即循环的开始前满足所有要求。2.保持:第n次循环都满足我们的要求。3.终止:循环结束符合我们的目的。
对于上面插入排序的函数InserctionSortTE代码:
for (int i = 1; i < len; i++) {
int temp = arr[i];
int j = i - 1;
for (; j >= 0 && temp < arr[j]; j--) {
arr[j + 1] = arr[j];
}
arr[j+1] = temp;
}
仍以排序一手扑克牌为例:在循环没有开始时,我们只拿到了一张扑克牌,它是排好序的,显然符合我们的要求;第n次循环时,假设前n-1次循环满足我们要求(即前面所有的扑克已经排好序),我们先创建一个临时变量temp存储拿来的扑克牌的值,然后用一个循环让它和前面已经排好序的扑克牌进行对比,如果temp更小,将temp向前移动否则 结束循环,显然这样操作后也就把扑克牌排好序了。这样
保持性也得到了证明。于是排序算法就得到了证明。看似没有价值,但这种思想对编程是很重要的。
接下来,我们来看一下经常看到的时间复杂度的问题。我们先来考虑最好的情况:假设说我们的扑克牌已经排好序了,我们依然用这种方法操作,在执行过程中将一直不满足内层循环的条件(temp<arr[j]),这样我们可以假设每次总循环的时间为常数c,总的循环次数显然是len-1表示,那么总共要花费的时间就是c(len-1);
我们再来考虑最坏的情况:假设每次内层循环的条件每次都得到满足(显然这就是最坏的情况),那么假设内层循环一次的时间是N’,循环次数显然是len的一个二次函数;
最短和最长的运行时间我们都知道了以后我们想用一个统一的标识符来表示这样算法的一个运行时间,我们用Θ(n平方)表示,(关于算法的渐进符号简单了解一下即可)。
至此,插入排序算法就介绍完了,我们肯定会去思考有没有更加高效的排序算法呢?
来来来~喝完这杯还有一杯