一、希尔排序
1、前面认识的几种排序算法,比如冒泡排序、简单选择排序和直接插入排序的时间复杂度都是O(n^2),而希尔排序(Shell Sort)的出现突破了时间复杂度O(n^2)。
2、在直接插入排序中,如果记录本身比较有序,或记录个数不多时,效率是很高的。但是这只是一种特例,针对大多数情况,还是需要O(n^2)的时间复杂度,但是可以人为的构造出记录比较有序或者记录个数不多的条件,比如将原本有大量记录数的记录列表进行分组,分割成若干个子序列,此时每个子序列待排序的记录个数就变的很少了,然后在这些子序列内分别进行直接插入排序,当整个序列都基本有序时(注意只是基本有序时),再对全体记录进行一次直接插入排序。比如有记录序列{9, 1, 5, 8, 3, 7, 4, 6, 2},分成三组{9, 1, 5}、{8, 3, 7}、{4, 6, 2},将它们各自排序变成{1, 5, 9}、{3, 7, 8}、{2, 4, 6},再合并成{1, 5, 9, 3, 7, 8, 2, 4, 6},此时还是杂乱无序,谈不上基本有序,要排序还是得重来一遍直接插入排序,所以所谓的基本有序指的是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,比如{2, 1, 3, 6, 4, 7, 5, 8, 9}就可以称为基本有序。
3、分割待排序记录的目的是减少待排序记录的个数,并使整个序列向基本有序发展。所以可以采取跳跃分割的策略:将相距某个"增量"的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
4、希尔排序算法实现:
#include <stdio.h>
// 定义待排序记录类型
typedef int RecordType;
/**
* 打印待排序记录数组
*/
void PrintRs(RecordType rs[])
{
int i, count = rs[0];
for (i = 1; i <= count; i++)
{
printf("%d ", rs[i]);
}
printf("\n");
}
/**
* 对记录列表进行希尔排序
*/
void ShellSort(RecordType *rs)
{
int i, j, count = rs[0];
int increment = count;
do
{
increment = increment / 3 + 1; // 增量
for (i = increment + 1; i <= count; i++)
{
if (*(rs + i) < *(rs + i - increment))
{
*rs = *(rs + i);
for (j = i - increment; j >= 1 && *(rs + j) > *rs; j -= increment)
{
*(rs + j + increment) = *(rs + j);
}
*(rs + j + increment) = *rs;
}
}
} while (increment > 1);
*rs = count; // 恢复下标0的元素为待排序记录个数
}
int main()
{
RecordType rs[] = {5, 1, 5, 3, 2, 4};
printf("排序前记录列表排序为:\t\t");
PrintRs(rs);
ShellSort(rs);
printf("从小到大排序后,记录列表排序为:");
PrintRs(rs);
return 0;
}
比如待排序记录序列为{1, 5, 3, 2, 4},第一次do...while循环时,增量为2,则第一次do...while循环内第一趟选取其中的{1, 3}来做直接插入排序,由于1<3不需要移动,结果不变; 第一次do...while循环内第二趟选取{5, 2}来做直接插入排序,由于5>2,所以将记录2临时保存到数组下标0的位置,记录5移动到原来记录2的位置,此时j=0,不满足条件,移动结束,再把保存在数组下标0的记录2移动到原来记录5所在位置,结果变为{1, 2, 3, 5, 4};第一次do...while循环内第三趟选取其中的{3, 4}来做直接插入排序,不需要移动,结果不变;接着,开始第二次do...while循环,此时增量变为1,第二次do...while循环内第一趟选取其中的{1, 2}做直接插入排序,不需要移动,结果不变;第二次do...while循环内第二趟选取{2, 3}来做直接插入排序,不需要移动,结果不变;第二次do...while循环内第三趟选取{3, 5}来做直接插入排序,不需要移动,结果不变;第二次do...while循环内第四趟选取{5, 4}来做直接插入排序,需要移动,首先将记录4暂存到下标0的位置,然后记录5移动到记录4原来的位置,然后j=3,但是下标3的记录3不大于下标0的记录4,移动结束,最后写回下标0的记录4到原来记录5的位置,结果变为{1, 2, 3, 4, 5},其实此时整个序列本身就是有序的了。 最后increment > 1不满足,整个循环结束,排序完成。
从实现上来看,希尔排序只不过是把待排序序列分成一些子序列来分别做直接插入排序,如此循环,直到增量最终一定会变为1,当增量为1时,其实整个序列就是基本有序的了,那么只用对整个序列做一次直接插入排序即可完成排序了。而在直接插入排序算法的实现中,只不过是将增量设置为1挨个处理而不需要外层的do...while循环来实现。这样的希尔排序就达到了分割的目的:使得待排序记录数量变少,从而减少比较次数与移动次数,从而提高了效率。
5、时间复杂度
希尔排序的关键并不是随便分组后各自排序,而是将相隔某个"增量"的记录组成一个子序列,实现跳跃式移动,使得效率提高,所以增量的选取是非常关键的,比如以上代码增量的选取语句为:increment = increment / 3 + 1,最后的1也是有意义的,使得在不断的循环过后增量一定会变为1(增量序列的最后一个增量值必须等于1),使得最后一次对整个基本有序的记录列表做最后一个直接插入排序,从而结束排序。并且增量的选取不能大于待排序记录个数,所以这里的除以3如果变成除以1也是不行的,而如果变成除以2的话,则有可能在某次循环后increment / 2 = 1,那么increment / 2 + 1 = 2,那么使得increment 永远满足increment > 1,所以除以2也是不行的。究竟该如何选取增量,没有一种最好的方式,不过大量研究表明,当增量为dlta[k] = 2^(t-k+1) - 1(0<=k<=t<=⌊log2(n+1))⌋时,可以获得不错的效果,其时间复杂度为O(n^(3/2)),好于直接插入排序O(n^2)。
6、稳定性
由于记录是跳跃式的移动,所以希尔排序并不是一种稳定的排序算法。
参考书籍:《大话数据结构》