希尔排序的简单实现
希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。
既然是插入排序的改进版本,我们就先来看一看插入排序。
插入排序的原理就和打牌时我们抓牌一样,每次摸到一张新的牌,我们都会将它插入到已经有序的牌序中,使之仍然有序。例如 我们 依次抽到 4 2 9 J 10 8 ,那么我们的牌序依次为 4 , 2 4 , 2 4 9 , 2 4 9 J , 2 4 8 9 10 J,
抽到10 为例,我们从后往前比较, 10 和 J 比较,10 比 J小, 所以 J 往后挪一个位置,然后 9 和10 比, 10 更大,说明找到了应在的位置,插入。
抽到最后的8 , 我们从后往前比较, 8 < J ,J往后挪, 8 < 10 , 10 往后挪, 8<9 ,9往后挪 , 8 >4,找到位置插入。
插入排序就是模拟了上述比较的过程。
可以看出,如果每次摸到一张新的牌,如果手中已有的最后一张牌比 刚摸得牌小,那么就不用继续比较了,这正好对应了插入排序的内层循环。
对应的先来看一下插入排序的源码
void Insertion_Sort(ElemType *A,int n){ for(int i = 1;i<n;i++){ ElemType tmp = A[i]; int j; for( j=i; j > 0 && A[j-1] > tmp ;j--) //① //当前序列的最后 往前与tmp比较 A[j] = A[j-1]; //遇到逆序就交换 A[j] = tmp; } }
我们知道插入排序的平均时间复杂度为O(n^2), 最佳时间复杂度为O(n),其效率的差异其实 就在内层循环①的执行次数上。可见如果初始序列有序或者说基本有序,那么插入排序的效率就非常高了,最佳就是O(n)。
但是插入排序每次只能交换相邻的一对元素,也就是说每次只能消除一个逆序对,而对于N个数,科学的研究表明:其逆序对的数目大致在 n(n-1) /4个,所以平均效率不可能超过O(n^2).
于是乎聪明的科学家们想到如果不交换相邻的元素那? 而是去交换相隔比较远的元素那?那么我们每次交换消除的逆序对就不止一个了!
希尔排序就是利用了这样的性质。
希尔排序设置了一个增量,每次通过缩小增量序列进行插入排序。
这个增量就是交换元素的间隔。
如上图
增量为5,也就是说5间隔的有 81 35 41 , 我们将这个子序列执行插入排序,变成 35 41 81 ,还有 94 17 75 变成了 17 75 94 , 11 95 15 变成了 11 15 96,
然后3间隔的进行排序,3间隔的排完序之后可以看出序列已经基本有序了,然后再将增量缩小为1,执行一次基本的插入排序,算法完成。
源代码
void shellSort(ElemType *a,int n){ for(int d = n/2;d>0;d/=2){ //希尔排序最初的增量分割 for(int i=d;i<n;i++){ ElemType tmp = a[i]; int j; for(j=i;j>=d && a[j-d] >tmp;j-=d) a[j] = a[j-d]; a[j] = tmp; } } }
对比插入排序,和希尔排序,可以看出希尔排序只是多了一层for循环,用来控制增量,而里面两层就是最基本的插入排序,只是将交换的位置 由 a[ j ] a [ j - 1 ] 变成了 a[ j ] a[ j - d ],
d就是当前的增量。
增量的选取
当然对增量的选取是十分重要的,上述代码是希尔最初的增量分割,每次将增量缩小一半,也就是(增量序列:1,2,4,8,16...)但是当遇到下面这种情况,时间复杂度就O(n^2)了。
显然,对于增量的选取只要最后一次增量变为1,那么一定可以使得序列有序。
已知的最好增量序列是由Sedgewick提出的(1, 5, 19, 41, 109,...),该序列的项来自
这两个公式算出。
感兴趣的同学自行实现或者问度娘吧。