目標:從小到大(或從大到小)對數組進行排序。
給你一組數組,將他們排序。插入排序算法的步驟如下:
把未排序的數字放在一堆。
從這堆數字里面挑一個。選哪個并不重要,但從頂部拿最容易。
將選出來的數字插入新數組中。
從未排序的數字堆中挑一個數字,并將其插入新數組。 在您選擇的第一個數字之前或之后,將這兩個數字排序。
再選擇一個數字,并按順序插入到數組中適當的位置。
重復這樣做,直到堆里沒有數字。 你最終得到一個空堆和一個排序的數組。
這就是為什么這被稱為“插入”排序,因為你從堆中取一個數字,并將其插入在數組里正確的排序位置。
舉個栗子
未排序的數字堆是 [ 8, 3, 5, 4, 6 ]
。
選擇第一個數字 8
,并將其插入新數組。 新數組是空的,所以很容易。 排序的數組現在是 [8]
,未排序的堆是 [ 3, 5, 4, 6 ]
。
從堆中選擇下一個數字 3
,并將其插入到排序的數組中。 它應該在8
之前,所以排序的數組現在 [ 3, 8 ]
,堆是 [ 5, 4, 6 ]
。
從堆中選擇下一個數字5
,并將其插入到排序的數組中。 它在3
和8
之間。排序的數組是 [ 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)
注釋對應的代碼分析:
創建數組的副本。這是必要的,因為我們不能直接修改參數
array
的內容。 像Swift 中的sort()
一樣,insertionSort()
函數將返回原始數組的副本,并將它排序。這個函數里有兩個循環:外部循環遍歷數組中的每個元素,從數字堆的最頂端挑選數字;變量
x
是排序區分排序不分與數字堆的索引(相當于|
的作用)。請記住,任何時候,array[0]
到array[x]
都是排序完成的。從array[x]
到結束,是未排序的數字堆。循環查看
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