很多希尔排序的视频和文字讲解,好像都没怎么讲清楚中间的过程到底是怎么样的。希尔排序,往往一时理解了,过很久再来看代码,就又有点搞不懂。
理解希尔排序首先要理解插入排序。希尔排序本身就是对插入排序的一个改进,因为当一个数组的大部分元素是从大到小排列,用插入排序来从小到大排序的话,效率会被降低(需要做很多次swap),这种情况用希尔排序就会提升排序效率。
插入排序 Insertion Sort
假设有arr = [6, 5, 4, 3, 2, 1]
,需要我们用插入排序来排序,那么过程如下:
我们可以把数组分成有序
和无序
两个部分,排序的时候,我们不断将无序部分的第一个元素插入有序部分。
[6] [5, 4, 3, 2, 1]
将5插入有序部分,5为数组的第2个元素(index = 1)。插入时,按照从后往前的顺序,逐一跟有序部分的元素比较,如果比有序元素的数值更小,就进行交换(swap);如果比有序元素的数值更大,就停止比较。
[5, 6] [4, 3, 2, 1]
将元素4(index = 2)插入[5, 6]
有序部分。按照从后往前的顺序,4先跟6交换,再跟5交换。
[4, 5, 6] [3, 2, 1]
将元素3(index = 3)插入[4, 5, 6]
,先后与6、5、4交换位置。
...
下面是C语言代码实现(用哪种语言都不重要了,因为代码看上去基本都会是一样的):
// 变量命名便于代码理解
void insertionSort(int arr[], int size)
{
int i, j, temp;
for(i = 1; i < size; i++)
for(j = i-1; j >= 0 && arr[j] > arr[j+1]; j--) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
希尔排序 Shell Sort
希尔排序就是按照一定的gap值,不断地对数组进行插入排序。不一样的希尔排序算法可能采用不一样的gap值。经典希尔算法的gap值为N/2, N/4, ...... 直到gap值为1,这里的N为数组的长度。
过程理解
前面提到,当一个数组的大部分元素是从大到小排序的时候,如果要用插入排序来实现从小到大排序,就需要做很多次swap,就像上面的[6, 5, 4, 3, 2, 1]
这个例子一样。这种情况下,希尔排序会先把数组处理成大部分元素都是从小到大排序,然后再用插入排序来最后处理,这样就能大量减少swap,提升排序效率。
我们用一个例子来看希尔排序工作的过程:
[61, 109, 149, 111, 34, 2, 24, 119, 122, 27]
首先,数组一共有10个元素(size = 10)。
第一轮,先用size/2做为间隔(gap),我们可以理解成是把原数组,按照间隔来分成了小数组,并对小数组进行插入排序。
这里gap = 5,小数组为[61, 2]、[109, 24]、[149, 119]、[111, 122]、[34, 27]。对小数组进行插入排序:[2, 61]、[24, 109]、[119, 149]、[111, 122]、[27, 34]。
我们并不改变原数组,所以排序后的原数组为[2, 24, 119, 111, 27, 61, 109, 149, 122, 34]
。
可以看到,经过一轮排序,原数组数值较大的一些元素到了数组的后面,这样能大大减少后面插入排序的swap次数。
第二轮,gap = gap/2 = 2。继续将原数组分为小数组,并对小数组进行插入排序。
[2, 119, 27, 109, 122]
、[24, 111, 61, 149, 34]
两个小数组排序后为
[2, 27, 109, 119, 122]
、[24, 34, 61, 111, 149]
因为不改变原数组,所以原数组这时为[2, 24, 27, 34, 109, 61, 119, 111, 122, 149]
第三轮,gap = gap/2 = 1。这是最后一轮,其实就是将第二轮排序后的数组,进行插入排序。
代码实现
当数组长度为n的时候,我们要做lg(n)
轮排序。lg(n)
轮排序,就是一个for loop,这个好理解。那么for loop里面,每一轮排序代码是什么样的?
第一轮:gap = 5,大的数组分成了5个小数组([61, 2]
、[109, 24]
、[149, 119]
、[111, 122]
、[34, 27]
)。
从index 5到9,要做5次插入(insertion)。
第二轮:gap = 2,大的数组分成了2个小数组([2, 119, 27, 109, 122]
、[24, 111, 61, 149, 34]
)。
从index 2到9, 要做8次插入(insertion)。
我们知道说,对一个数组进行插入排序的时候,每一次插入,都要跟前面有序的元素进行对比,但一旦比有序元素更大,就停止对比,完成插入。
序号为2的元素先是跟0比较,序号2的值比序号0的值大,不需要swap,完成插入;
序号为4的先跟序号2先比较,因为比2小,所以要swap,再跟序号0比较;
序号为6的先跟序号4比较,swap,再跟序号2比较,停止并完成插入;
序号为8的先跟序号6比较,因为比6大,直接停止比较。
# include <stdio.h>
void shellsort(int arr[], int n) {
int gap, i, j, temp;
for(gap = n/2; gap > 0; gap /= 2)
for(i = gap; i < n; i++)
for(j = i - gap; j >= 0 && arr[j] > arr[j+gap]; j -= gap) {
temp = arr[j];
arr[j] = arr[j+gap];
arr[j+gap] = temp;
}
}
int main() {
int arr[] = {61, 109, 149, 111, 34, 2, 24, 119, 122, 27};
int size = sizeof arr / sizeof arr[0];
shellsort(arr, size);
for(int i = 0; i < size; i++) {
printf("i: %d\n", arr[i]);
}
return 0;
}