【学习笔记】最容易理解的希尔排序(shell sort)讲解

6,374 阅读5分钟

很多希尔排序的视频和文字讲解,好像都没怎么讲清楚中间的过程到底是怎么样的。希尔排序,往往一时理解了,过很久再来看代码,就又有点搞不懂。

理解希尔排序首先要理解插入排序。希尔排序本身就是对插入排序的一个改进,因为当一个数组的大部分元素是从大到小排列,用插入排序来从小到大排序的话,效率会被降低(需要做很多次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)。

1623724644(1).png

第一轮,先用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里面,每一轮排序代码是什么样的?

1623724644(1).png

第一轮:gap = 5,大的数组分成了5个小数组([61, 2][109, 24][149, 119][111, 122][34, 27])。
从index 5到9,要做5次插入(insertion)。

1623726278(1).png

第二轮: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大,直接停止比较

1623726297(1).png

# 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; 
}