插入排序是排序入门最基础的算法之一,其原理也是简单粗暴的:为第I个元素选择正确的位置,前提是保证前(i-1)个元素已经排序完成,元素i遍历它之前的(i-1)元素,并插入到正确的位置中去,这就是“插入排序”的名字由来。关于插入排序各类算法和数据结构的书籍都会有详细描述,这里不再熬述。废话少说,放码过来。
#include <iostream>
#include <windows.h>
using namespace std;
const int NUM = 20000;
double insertSort1() {
LARGE_INTEGER freq;
LARGE_INTEGER st, et;
int *arr = new int[NUM];
for(int i = 0; i < NUM; i++) {
arr[i] = NUM - i;
}
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&st);
for(int i = 1; i < NUM; i++) {
int tmp = arr[i]; //记住当前arr[i]的值
int k = i; //用来标记arr[i]要插入的位置
for(int j = i - 1; j >= 0; j--){
if(arr[j] > tmp) { //当元素arr[j]比arr[i]的值大,则需将arr[j]向下标增大的方向移动,腾出arr[j]的位置
arr[j+1] = arr[j];
k = j; //用k保存arr[j]腾出来的位置
}
}
arr[k] = tmp; //将tmp的值插入正确的位置
}
QueryPerformanceCounter(&et);
double res = 1000.0*(et.QuadPart - st.QuadPart)/freq.QuadPart;
return res;
}
double insertSort2() {
LARGE_INTEGER freq;
LARGE_INTEGER st, et;
int *arr = new int[NUM];
for(int i = 0; i < NUM; i++) {
arr[i] = NUM - i;
}
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&st);
for(int i = 1; i < NUM; i++) {
int tmp = arr[i];
int j;
for(j = i - 1; j >= 0 && arr[j] > tmp; j--){
arr[j+1] = arr[j];
}
arr[j+1] = tmp;
}
QueryPerformanceCounter(&et);
double res = 1000.0*(et.QuadPart - st.QuadPart)/freq.QuadPart;
return res;
}
int main()
{
int pass = 10;
double sum1 = 0;
double sum2 = 0;
for(int i = 0; i < pass; i++) {
sum1 += insertSort1();
sum2 += insertSort2();
}
cout<<"sort1 aveTime: "<<sum1/10<<endl;
cout<<"sort2 aveTime: "<<sum2/10<<endl;
}
上面代码中,insertSort1和insertSort2都是插入排序(升序)的实现,insertSort2是稍微优化过的版本。在insertSort1中,关键部分的代码已经写上注释,大概流程是:
1)初始化大小为NUM的整型数组,倒序排放。NUM大小可根据自己的需求修改,一般来说(MIT公开课里的教授说的啊),NUM的数量在30以下,插排的性能就非常优秀,这也是为什么shell sort排序到一定程度后可以采用插排来整理的原因,但NUM大于30的数量性能瓶颈就开始明显了。
2)使用插入排序算法排序
3)计算执行排序的代码所执行的时间
之所以选择数组倒序是因为这是插入排序效率最差的情况,相当于每为一个元素找到正确位置(也即是数组首元素),你就必须比较和移动当前元素之前的每一个元素,复杂度为O(n^2),这样避免了测试insertSort1和insertSort2两个版本的插排算法是由于随机数组的不同而产生的差距。在main函数中,将每个算法运行10遍,求平均值。测试的过程中,我修改过NUM的数量级,30,100,1000,5000,10000,20000,insertSort2的性能都是要优于insertSort1的,图1为多次运行程序后某一次的测试结果截图,没有意外,每次都是2的耗时低于1。
图1
原因:在算法1中,为每个arr[i]找到正确位置j之后,没有任何跳出循环的操作,会继续将arr[i]跟j位置以前的元素做比较,然而正确的位置已找到,并由于数组i之前的元素已经排好序,故正确位置不会再改变,也就是说这些比较都是无用功,可避免的。在算法2中,将这个比较的条件放在循环的判断条件里,一旦找到正确位置之后,判断条件不成立而退出循环体,故不会再执行后面那些无用的比较。但是,这个原因在上述代码中应该不存在,因为我采用的是倒序数组,无论如何对每一个元素i,由于正确的位置总是在数组首元素位置,因此一定是做(i-1)次比较和数据赋值。因此,操作这两者的原因我猜想是在算法1中每次循环比算法2多执行了一行“k = j”的用来保存正确位置的代码,造成了耗时多于算法2。无论如何,算法2都要比算法1更优一些。
附:在C++中,用来测试某些代码执行所耗费的时间,可以采用上述代码中采用的QPC(QueryPerformanceCounter),QPC的用法可以查看MSDN或者这里有示例。在C++中计时还可以使用<time.h>中定义的clock()和GetTickCount(),这两者的计时是基于Timer的,缺点是精度不足,我在测试上述两个算法的差别是,在NUM为1000以下都显示为0毫秒;而QPC是基于Counter的,但也有自己的缺点,并且在MSDN也有官方对其产生的bug进行修补的方法。这篇文章及其补充有对这几种计时方法进行介绍。