Swift算法俱樂部中文版 -- 插入排序

目標:從小到大(或從大到小)對數組進行排序。

給你一組數組,將他們排序。插入排序算法的步驟如下:

  • 把未排序的數字放在一堆。

  • 從這堆數字里面挑一個。選哪個并不重要,但從頂部拿最容易。

  • 將選出來的數字插入新數組中。

  • 從未排序的數字堆中挑一個數字,并將其插入新數組。 在您選擇的第一個數字之前或之后,將這兩個數字排序。

  • 再選擇一個數字,并按順序插入到數組中適當的位置。

  • 重復這樣做,直到堆里沒有數字。 你最終得到一個空堆和一個排序的數組。

這就是為什么這被稱為“插入”排序,因為你從堆中取一個數字,并將其插入在數組里正確的排序位置。

我們摸撲克牌就是插入排序!

舉個栗子


未排序的數字堆是 [ 8, 3, 5, 4, 6 ]

選擇第一個數字 8 ,并將其插入新數組。 新數組是空的,所以很容易。 排序的數組現在是 [8] ,未排序的堆是 [ 3, 5, 4, 6 ]

從堆中選擇下一個數字 3 ,并將其插入到排序的數組中。 它應該在8之前,所以排序的數組現在 [ 3, 8 ] ,堆是 [ 5, 4, 6 ]

從堆中選擇下一個數字5,并將其插入到排序的數組中。 它在38之間。排序的數組是 [ 3, 5, 8 ] ,堆是 [ 4, 6 ]

重復此過程,直到堆是空的。

在一個數組中排序


上面的解釋會你看起來需要兩個數組:一個用于未排序的數組,一個排序好的數組。

但是你可以在一個數組里執行插入排序,而無需創建新的數組。 你只需知道數組的哪個部分已經排序,哪個部分是未排序的堆。

最初,數組是 [ 8, 3, 5, 4, 6 ] 。用 | 區別排序部分和未排序部分:

[| 8, 3, 5, 4, 6 ]

這表示排序部分是空的,未排序的數字堆從8開始。

處理完第一個數字后,是這樣的:

[ 8 | 3, 5, 4, 6 ]

排序部分是 [ 8 ] ,堆是 [ 3, 5, 4, 6 ]| 已經向右移位了一位。

這是數組的排序過程:

[| 8, 3, 5, 4, 6 ]
[ 8 | 3, 5, 4, 6 ]
[ 3, 8 | 5, 4, 6 ]
[ 3, 5, 8 | 4, 6 ]
[ 3, 4, 5, 8 | 6 ]
[ 3, 4, 5, 6, 8 |]

在每個步驟中,| 從數組的開頭,一下一下的向后移動,直到排序完成時到達數組的結尾。 數字堆一個個縮小,并且排序部分一個個增加,直到數字堆是空的,并且所有數字都排序完成。

怎樣插入?


每個步驟,都要從未排序的堆中選擇最頂層的數字,并將其插入排序數組。 您必須把數字放在合適的位置,保證數組是排序好的。 怎樣做呢?

我們假設已經做了幾步,數組看起來像這樣:

[ 3, 5, 8 | 4, 6 ]

下一個要排序的數字是 4 ,排序好的部分是 [ 3, 5, 8 ]

用這種方法:看看上一個元素 8

[ 3, 5, 8, 4 | 6 ]
        ^

8 是否大于 4 ? 是的,所以 4 應該在 8 之前。我們交換這兩個數字得到:

[ 3, 5, 4, 8 | 6 ]
        <-->
        交換

還沒完。 新的前一個元素 5 也大于 4 。我們還交換這兩個數字:

[ 3, 4, 5, 8 | 6 ]
     <-->
     交換

再看看前面的元素。 3 大于 4 嗎? 不是。 這意味著我們完成了 4 在數組中的排序。

這是插入排序算法,您將在下一節中看到內部循環的描述部分:通過交換數字將從堆的頂部的數字插入到排序的部分。

代碼


這是用 Swift 實現的插入排序:

func insertionSort(_ array: [Int]) -> [Int] {
    var a = array                           // 1
    for x in 1..<a.count {                  // 2
        var y = x
        while y > 0 && a[y] < a[y - 1] {    // 3
            swap(&a[y - 1], &a[y])
            y -= 1
        }
    }
    return a
}

把這段代碼放到 playground 里測試:

let list = [ 10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26 ]
insertionSort(list)

注釋對應的代碼分析:

  1. 創建數組的副本。這是必要的,因為我們不能直接修改參數 array 的內容。 像Swift 中的 sort()一樣,insertionSort() 函數將返回原始數組的副本,并將它排序。

  2. 這個函數里有兩個循環:外部循環遍歷數組中的每個元素,從數字堆的最頂端挑選數字;變量 x 是排序區分排序不分與數字堆的索引(相當于 | 的作用)。請記住,任何時候,array[0]array[x] 都是排序完成的。從 array[x] 到結束,是未排序的數字堆。

  3. 循環查看 array[x] ,這是在數字堆中頂部的數組,它可能小于排序數組的任何元素。循環排序好的數組,每次往前找一個,如果前面的更大,就交換它們。當循環完成時,排序好的數組就增長了一個元素。

注意:外循環從(注釋2處)索引從1開始,將第一個元素從數字堆中移動到排序部分并不會改變任何內容,所以我們可以跳過它。

減少交換


上面的插入排序雖然能使用,但是如果能刪除 swap() 方法會運行的更快。

您已經看到,我們交換數字,將下一個元素根據排序移動到合適的位置:

[ 3, 5, 8, 4 | 6 ]
        <-->
        swap

[ 3, 5, 4, 8 | 6 ]
     <-->
     swap

我們可以不交換,將數字向右移動,然后把新數字放到合適的位置就可以了。

[ 3, 5, 8, 4 | 6 ]  復制 4
           *

[ 3, 5, 8, 8 | 6 ]  將 8 向右移動
        --->

[ 3, 5, 5, 8 | 6 ]  將 5 向右移動
     --->

[ 3, 4, 5, 8 | 6 ]  把 4 粘貼到合適的位置
     *

代碼:

func insertionSort(_ array: [Int]) -> [Int] {
    var a = array
    for x in 1..<a.count {
        var y = x
        let temp = a[y]
        while y > 0 && temp < a[y - 1] {
            a[y] = a[y - 1]                 // 1
            y -= 1
        }
        a[y] = temp                         // 2
    }
    return a
}

// 1 處是將數字向右移動一位。內循環結束時,y 是排序數組中和新數字比較的索引。// 2 處是將新數字復制到對應的位置。

給其他類型排序


我們可以給其他類型排序,添加泛型并提供比較函數即可。只需要修改兩處代碼。

方法定義變為:

func insertionSort<T>(_ array: [T], _ isOrderedBefore: (T, T) -> Bool) -> [T] {

數組具有泛型 [T],現在 isOrderedBefore() 方法將接受任何類型,無論是數組,字符串,或是其他類型。

新的參數 isOrderedBefore: (T, T) -> Bool 是一個方法 ,接受兩個 T 類型的對象,如果兩個參數比較厚滿足這個表達式,返回 true,不滿足則返回 false。就像是 Swift 內置的 sort() 方法。

另一個改動的地方是在內循環:

    while y > 0 && isOrderedBefore(temp, a[y - 1]) {

temp < a[y - 1] 替換為 isOrderedBefore() 方法。它做了同樣的事情,并且可以比較任何類型。

在 Playground 中測試:

let numbers = [ 10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26 ]
insertionSort(numbers, <)
insertionSort(numbers, >)

<> 決定排序順序,分別是從小到大和從大到小。

當然,你也可以排序其他類型,如字符串:

let strings = [ "b", "a", "d", "c", "e" ]
insertionSort(strings, <)

甚至是更復雜的對象:

let objects = [ obj1, obj2, obj3, ... ]
insertionSort(objects) { $0.priority < $1.priority }

閉包是告訴 insertionSort() 根據對象的屬性 priority 進行排序。

插入排序是一種穩定的排序。對于都具有排序鍵的元素,是相對穩定的。這對于簡單的數字或字符串不重要 ,但對于復雜對象就非常重要了。在上面的例子中,兩個對象的 priority 一樣的話,就不會交換,不管其他屬性的值多大多小。

性能


如果數組已經排序,那插入排序真的很快。這聽起來很明顯,但不是所有的排序算法都能做到。在實踐中,如果是大量數據并不完全排序,插入排序會表現的很好。

最壞情況下插入排序的平均情況性能是 O(n ^ 2) 。 這是因為在這個函數中有兩個嵌套循環。 其他排序算法(如快速排序和合并排序)的性能是 O(n log n),在數據量較大時更快。

插入排序實際上對排序小數組非常快。 當元素個數為10或更小需要排序的時候,一些標準庫會把它們的排序方法從快速排序切換到插入排序。

我做了一個簡單的測試比較我們的 insertionSort() 與 Swift 的內置 sort() 。 在大約100個元素左右的數組上,速度的差異很小。 然而,隨著個數增加,O(n ^ 2) 很快開始執行比 O(n log n) 差很多,并且插入排序不能跟上。

作者:Matthijs Hollemans -- Swift 算法俱樂部

英文鏈接:
https://github.com/raywenderlich/swift-algorithm-club/tree/master/Insertion%20Sort

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容