數據結構和算法(Golang實現)(22)排序算法-希爾排序

希爾排序

1959 年一個叫Donald L. Shell (March 1, 1924 – November 2, 2015)的美國人在Communications of the ACM 國際計算機學會月刊發佈了一個排序算法,今後名爲希爾排序的算法誕生了。算法

注:ACM = Association for Computing Machinery,國際計算機學會,世界性的計算機從業員專業組織,創立於1947年,是世界上第一個科學性及教育性計算機學會。segmentfault

希爾排序是直接插入排序的改進版本。由於直接插入排序對那些幾乎已經排好序的數列來講,排序效率極高,達到了O(n)的線性複雜度,可是每次只能將數據移動一位。希爾排序創造性的能夠將數據移動n位,而後將n一直縮小,縮到與直接插入排序同樣爲1,請看下列分析。數組

希爾排序屬於插入類排序算法。數據結構

1、算法介紹

有一個N個數的數列:併發

  1. 先取一個小於N的整數d1,將位置是d1整數倍的數們分紅一組,對這些數進行直接插入排序。
  2. 接着取一個小於d1的整數d2,將位置是d2整數倍的數們分紅一組,對這些數進行直接插入排序。
  3. 接着取一個小於d2的整數d3,將位置是d3整數倍的數們分紅一組,對這些數進行直接插入排序。
  4. ...
  5. 直到取到的整數d=1,接着使用直接插入排序。

這是一種分組插入方法,最後一次迭代就至關因而直接插入排序,其餘迭代至關於每次移動n個距離的直接插入排序,這些整數是兩個數之間的距離,咱們稱它們爲增量。數據結構和算法

咱們取數列長度的一半爲增量,之後每次減半,直到增量爲1。函數

舉個簡單例子,希爾排序一個 12 個元素的數列:[5 9 1 6 8 14 6 49 25 4 6 3],增量d的取值依次爲:6,3,1code

x 表示不須要排序的數


取 d = 6 對 [5 x x x x x 6 x x x x x] 進行直接插入排序,沒有變化。
取 d = 3 對 [5 x x 6 x x 6 x x 4 x x] 進行直接插入排序,排完序後:[4 x x 5 x x 6 x x 6 x x]。
取 d = 1 對 [4 9 1 5 8 14 6 49 25 6 6 3] 進行直接插入排序,由於 d=1 徹底就是直接插入排序了。

越有序的數列,直接插入排序的效率越高,希爾排序經過分組使用直接插入排序,由於步長比1大,在一開始能夠很快將無序的數列變得不那麼無序,比較和交換的次數也減小,直到最後使用步長爲1的直接插入排序,數列已是相對有序了,因此時間複雜度會稍好一點。協程

在最好狀況下,也就是數列是有序時,希爾排序須要進行logn次增量的直接插入排序,由於每次直接插入排序最佳時間複雜度都爲:O(n),所以希爾排序的最佳時間複雜度爲:O(nlogn)排序

在最壞狀況下,每一次迭代都是最壞的,假設增量序列爲:d8 d7 d6 ... d3 d2 1,那麼每一輪直接插入排序的元素數量爲:n/d8 n/d7 n/d6 .... n/d3 n/d2 n,那麼時間複雜度按照直接插入的最壞複雜度來計算爲:

假設增量序列爲 ⌊N/2⌋ ,每次增量取值爲比上一次的一半小的最大整數。

O( (n/d8)^2 + (n/d7)^2 + (n/d6)^2 + ... + (n/d2)^2 + n^2)

= O(1/d8^2 + 1/d7^2 + 1/d6^2 + ... + 1/d2^2 + 1) * O(n^2)
= O(等比爲1/2的數列和) * O(n^2)
= O(等比求和公式) * O(n^2)
= O( (1-(1/2)^n)/(1-1/2) ) * O(n^2)
= O( (1-(1/2)^n)*2 ) * O(n^2)
= O( 2-2*(1/2)^n ) * O(n^2)
= O( < 2 ) * O(n^2)

因此,希爾排序最壞時間複雜度爲O(n^2)

不一樣的分組增量序列,有不一樣的時間複雜度,可是沒有人可以證實哪一個序列是最好的。Hibbard增量序列:1,3,7,···,2n−1是被證實可普遍應用的分組序列,時間複雜度爲:Θ(n^1.5)

希爾排序的時間複雜度大約在這個範圍:O(n^1.3)~O(n^2),具體還沒法用數學來嚴格證實它。

希爾排序不是穩定的,由於每一輪分組,都使用了直接插入排序,但分組會跨越n個位置,致使兩個相同的數,發現不了對方而產生了順序變化。

2、算法實現

package main

import "fmt"

// 增量序列折半的希爾排序
func ShellSort(list []int) {
    // 數組長度
    n := len(list)

    // 每次減半,直到步長爲 1
    for step := n / 2; step >= 1; step /= 2 {
        // 開始插入排序,每一輪的步長爲 step
        for i := step; i < n; i += step {
            for j := i - step; j >= 0; j -= step {
                // 知足插入那麼交換元素
                if list[j+step] < list[j] {
                    list[j], list[j+step] = list[j+step], list[j]
                    continue
                }
                break
            }
        }
    }
}

func main() {
    list := []int{5}
    ShellSort(list)
    fmt.Println(list)

    list1 := []int{5, 9}
    ShellSort(list1)
    fmt.Println(list1)

    list2 := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3}
    ShellSort(list2)
    fmt.Println(list2)

    list3 := []int{5, 9, 1, 6, 8, 14, 6, 49, 25, 4, 6, 3, 2, 4, 23, 467, 85, 23, 567, 335, 677, 33, 56, 2, 5, 33, 6, 8, 3}
    ShellSort(list3)
    fmt.Println(list3)
}

輸出:

[5]
[5 9]
[1 3 4 5 6 6 6 8 9 14 25 49]
[1 2 2 3 3 4 4 5 5 6 6 6 6 8 8 9 14 23 23 25 33 33 49 56 85 335 467 567 677]

按照以前分析的幾種排序算法,通常建議待排序數組爲小規模狀況下使用直接插入排序,在規模中等的狀況下可使用希爾排序,但在大規模仍是要使用快速排序,歸併排序或堆排序。

系列文章入口

我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook

相關文章
相關標籤/搜索