插入排序总体上说也是简单排序,除去Shell排序的话,其他的插入排序算法时间复杂度基本上是O(n ^ 2)的,除非有特殊的情况.
首先说说直接插入排序:
描述:
插入排序的思想大概就像是打牌是摸牌的过程(很多书诸如<<算法导论>>和<<计算机程序设计艺术>>都那这个例子来打比方,好像大师们都喜欢玩桥牌,我不是大师所以只喜欢打够级).
玩牌者新摸到牌时总是要搜索一下然后把牌插到适合的位置,慢慢地,牌摸完时,手中地牌自然而然地排好了顺序.
代码:
#include
<
stdio.h
>

const
int
MAX
=
10
;

void
InsertSort(
int
*
A)

...
{
int i, j, temp;
for(i = 1; i < MAX; i++)

...{
temp = A[i];
j = i - 1;
while( j >= 0 && temp < A[j])

...{
A[j + 1] = A[j];
j--;
}
A[j + 1] = temp;
}
}

int
main()

...
{
int A[MAX];
int i;
int nums = MAX;
printf("Please input %d numbers: ", nums);
for(i = 0; i < MAX; i++)
scanf("%d", &A[i]);
InsertSort(A);
for(i = 0; i < MAX; i++)
printf("%d ", A[i]);
return 0;
}
分析:
直接插入排序的好处就是算法简单明了,程序容易实现.但是效率就不可避免的比较低下.程序除了需要一个记录数据的数组以外还要有一个临时的辅助空间.
从程序中可以看出,即使假设数组已经是顺序的,程序仍旧需要进行n - 1次的比较,但是不用进行数组元素的移动.当数组是逆序的时候,程序需要进行(n + 2)(n - 1) / 2次的比较,另加(n + 4)(n - 1) / 2次的数组元素移动.综上平均计算一下的话,差不多要进行(n ^ 2) / 4次的比较和移动,所以复杂度应该是O(n ^ 2).
折半插入排序:
鉴于直接插入排序再处理第i个数据的时候,平均要和i / 2个已经排好序的数据进行比较,如果有n个元素的话,那么总的计算起来平均就是(n ^ 2) / 4.我们可以把二分查找和直接插入排序结合起来.用二分查找的技术来寻找应该插入的正确位置.
代码:
#include
<
stdio.h
>

const
int
MAX
=
10
;

void
BinaryInsertSort(
int
*
A)

...
{
int i, j, temp, first, last, mid;
for(i = 1; i < MAX; i++)

...{
temp = A[i];
first = 0;
last = i - 1;
while(first <= last) //用二分查找得方法查找key应该插入的位置

...{
mid = (first + last) / 2;
if(A[mid] > temp)
last = mid - 1;
else
first = mid + 1;
}
j = i -1;
while(j > last && temp < A[j])

...{
A[j + 1] = A[j];
j--;
}
A[last + 1] = temp;
}
}

int
main()

...
{
int A[MAX];
int i;
int nums = MAX; // Just for the next sentence。
printf("Please input %d numbers: ", nums);
for(i = 0; i < MAX; i++)
scanf("%d", &A[i]);
BinaryInsertSort(A);
for(i = 0; i < MAX; i++)
printf("%d ", A[i]);
return 0;
}
这个方法可以减少数据比较的次数,但是数据移动还是要一步一步的进行(仍旧需要移动大约i / 2个已排好序的元素),所以时间复杂度没有什么实质的改善.治标不治本的方法.
二路插入排序:
上面的二分插入减少的是数据比较的次数,而二路插入的方法可以减少数据移动的次数.
代码:
#include
<
stdio.h
>

const
int
MAX
=
10
;

void
twoWaysInsertSort(
int
*
A)

...
{
int first, final, i, keyIndex, index;
int B[MAX]; //辅助记录空间
first = final = 0;
B[0] = A[0];
for(i = 1; i < MAX; i++)

...{
if(A[i] >= B[final])

...{
final += 1;
B[final] = A[i];
}
else if(A[i] < B[first])

...{
first = (first + MAX - 1) % MAX;
B[first] = A[i];
}
else //当数据在最大值和最小值之间时

...{
keyIndex = (final - 1 + MAX) % MAX;
while(1)

...{
if (B[keyIndex] <= A[i])

...{
index = final++;
while(index != keyIndex)

...{
B[(index + 1) % MAX] = B[index];
index = (index - 1 + MAX) % MAX;
}
B[(keyIndex + 1) % MAX] = A[i];
break;
}
keyIndex = (keyIndex - 1 + MAX) % MAX;
}
}
}
for(i = 0; i < MAX; i++)

...{
printf("%d ", B[i]);
}
printf(" ");
for(i = 0; i < MAX; i++) //将辅助空间中的排好序的数组复制到原数组中

...{
A[i] = B[first];
first = (first + 1) % MAX;
}
}

int
main()

...
{
int A[MAX];
int i;
int nums = MAX; // Just for the next sentence。
printf("Please input %d numbers: ", nums);
for(i = 0; i < MAX; i++)
scanf("%d", &A[i]);
twoWaysInsertSort(A);
for(i = 0; i < MAX; i++)
printf("%d ", A[i]);
return 0;
}
这个二路插入的算法可以比二分插入算法节约一半的运行时间,但是程序比较复杂.
上面的程序另外使用了一个大小为n的辅助空间,并且把B看成了循环向量.first是数组中最小的元素,final是数组中最大的元素,如果待排序的元素小于first则放在first的前面,如果比final小则放在final的后面.
在<<计算机程序设计艺术>>中作者说到过一种方法,把输入区域当作一个循环表,而且位置N同1相邻.根据上一次插入的元素是落在已排序元素的中心的左面还是右面,而从当前未排序元素段的右面或是左面来取新的元素.过后通常需要"转动"这个区域.通过这个方法可以仅用N + 1的空间同时进行输入,输出和排序.
插入排序族中还有许多其他的方法,但是基本上都是在直接插入的方法上改进的,目的无非是减少比较和移动的次数.Shell排序也可以算是一种插入排序.不同的是Shell排序的时间复杂度不是O(n ^ 2),而是根据所选的增量序列而定的.