以前的文章中,我們主要是在講數據結構:比如數組、鏈表、隊列、樹。這些數據結構都是了解Swift和算法的基礎。從今以后的文章,我們將更多的關注于通用算法,這次我們就來聊聊排序。這次的主要內容有:
- 基本概念
- 排序實戰
基本概念
我們平常用的排序算法一般就以下幾種:
名稱 | 時間復雜度 | 空間復雜度 | 是否穩定 |
---|---|---|---|
冒泡排序 | O(n^2) | O(1) | 是 |
插入排序 | O(n^2) | O(1) | 是 |
選擇排序 | O(n^2) | O(1) | 否 |
堆排序 | O(nlogn) | O(1) | 否 |
歸并排序 | O(nlogn) | O(n) | 是 |
快速排序 | O(nlogn) | O(1) | 否 |
桶排序 | O(n) | O(k) | 是 |
這些算法具體的定義本文不再贅述。一般情況下,好的排序算法性能是O(nlogn),壞的性能是O(n^2)。本文在此用swift示范實現歸并排序:
func mergeSort(array: [Int]) -> [Int] {
var helper = Array(count: array.count, repeatedValue: 0)
var array = array
mergeSort(&array, &helper, 0, array.count - 1)
return array
}
func mergeSort(inout array: [Int], inout _ helper: [Int], _ low: Int, _ high: Int) {
guard low < high else {
return
}
let middle = (high - low) / 2 + low
mergeSort(&array, &helper, low, middle)
mergeSort(&array, &helper, middle + 1, high)
merge(&array, &helper, low, middle, high)
}
func merge(inout array: [Int], inout _ helper: [Int], _ low: Int, _ middle: Int, _ high: Int) {
// copy both halves into a helper array
for i in low ... high {
helper[i] = array[i]
}
var helperLeft = low
var helperRight = middle + 1
var current = low
// iterate through helper array and copy the right one to original array
while helperLeft <= middle && helperRight <= high {
if helper[helperLeft] <= helper[helperRight] {
array[current] = helper[helperLeft]
helperLeft += 1
} else {
array[current] = helper[helperRight]
helperRight += 1
}
current += 1
}
// handle the rest
guard middle - helperLeft >= 0 else {
return
}
for i in 0 ... middle - helperLeft {
array[current + i] = helper[helperLeft + i]
}
}
表格中有一個特例是桶排序,它是將輸入的數組分配到一定數量的空桶中,每個空桶再單獨排序。當輸入的數組是均勻分配時,桶排序的時間復雜度為O(n)。舉個微軟的面試題來當例子:
有三種顏色(紅,黃,藍)的球若干,要求將所有紅色的球放在黃色球的前面,最后放上所有的藍色球。
這道題目最直接的解法就是桶排序。首先第一次遍歷,統計有多少個紅色球(假設x個),多少個黃色球(假設y個),和多少個藍色球(假設z個)。然后再一次遍歷,數組前部x個位置填充紅色球,中間y個位置放上對應數量的黃色球,最后z個位置再放上藍色球。
另外解釋一下穩定的意思:相等的鍵值,如果排過序后與原來未排序的次序相同,則稱此排序算法為穩定。舉個例子:
// 原數組
[[2, 1], [1,3], [1,4]]
// 排序算法一
[[1,3], [1,4], [2, 1]]
// 排序算法二
[[1,4], [1,3], [2, 1]]
我們注意到排序算法一和二的區別就在于對[1, 3], [1, 4]這兩個元素的處理。排序算法一中,這兩個元素位置與原數組相同,故稱其為穩定算法。而排序算法二則是不穩定算法。
Swift中,排序的使用如下:
// 以升序排列為例,原數組可改變
array.sort
// 以降序排列為例,原數組不可改變
newArray = array.sorted(by: >)
// 字典鍵值排序示例
let keys = Array(map.keys)
let sortedKeys = keys.sorted() {
return map[$0]! > map[$1]!
}
在其他語言比如Java中,其自帶的sort函數是用歸并排序實現的。而在Swift源代碼中,sort函數采用的是一種內審算法(IntroSort)。它由堆排序、插入排序、快速排序三種算法構成,依據輸入的深度相應選擇最佳的算法來完成。本文關注的重點是實戰,所以不做展開。對源代碼感興趣的朋友可以去Github讀蘋果的Swift的開源庫。
排序實戰
直接來看一道Facebook, Google, Linkedin都考過的面試題。
已知有很多會議,如果有這些會議時間有重疊,則將它們合并。
比如有一個會議的時間為3點到5點,另一個會議時間為4點到6點,那么合并之后的會議時間為3點到6點
解決算法題目第一步永遠是把具體問題抽象化。這里每一個會議我們已知開始時間和結束時間,就可以寫一個類來定義它:
public class MeetingTime {
public var start: Int
public var end: Int
public init(_ start: Int, _ end: Int) {
self.start = start
self.end = end
}
}
然后題目說已知有很多會議,就是說我們已知有一個MeetingTime的數組、所以題目就轉化為寫一個函數,輸入為一個MeetingTime的數組,輸出為一個將原數組中所有重疊時間都處理過的新數組。
func merge(meetingTimes: [MeetingTime]) -> [MeetingTime] {}
下面來分析一下題目怎么解。最基本的思路是遍歷一次數組,然后歸并所有重疊時間。舉個例子:[[1, 3], [5, 6], [4, 7], [2, 3]]。這里我們可以發現[1, 3]和[2, 3]可以歸并為[1, 3],[5, 6]和[4, 7]可以歸并為[5, 7]。所以這里就提出一個要求:要將所有可能重疊的時間盡量放在一起,這樣遍歷的時候可以就可以從前往后一個接著一個的歸并。于是很自然的想到 -- 按照會議開始的時間排序。
這里我們要對一個class進行排序,而且要自定義排序方法,在Swift中可以這樣寫:
meetingTimes.sortInPlace() {
if $0.start != $1.start {
return $0.start < $1.start
} else {
return $0.end < $1.end
}
}
意思就是首先對開始時間進行升序排列,如果它們相同,就比較結束時間。
有了排好順序的數組,要得到新的歸并后的結果數組,我們只需要在遍歷的時候,每次比較原數組(排序后)當前會議時間與結果數組中當前的會議時間,假如它們有重疊,則歸并;如果沒有,則直接添加進結果數組之中。所有代碼如下:
func merge(meetingTimes: [MeetingTime]) -> [MeetingTime] {
// 處理特殊情況
guard meetingTimes.count > 1 else {
return meetingTimes
}
// 排序
var meetingTimes = meetingTimes.sort() {
if $0.start != $1.start {
return $0.start < $1.start
} else {
return $0.end < $1.end
}
}
// 新建結果數組
var res = [MeetingTime]()
res.append(meetingTimes[0])
// 遍歷排序后的原數組,并與結果數組歸并
for i in 1..<meetingTimes.count {
let last = res[res.count - 1]
let current = meetingTimes[i]
if current.start > last.end {
res.append(current)
} else {
last.end = max(last.end, current.end)
}
}
return res
}
展望
排序在Swift中的應用場景很多,比如tableView中對于dataSource的處理。當然很多時候,排序都是和搜索,尤其是二分搜索配合使用。下期探討搜索的時候,會對排序進行進一步拓展。