1. 程式人生 > >Swift實現的快速排序及sorted方法的對比

Swift實現的快速排序及sorted方法的對比

Swift語言有著優秀的函數語言程式設計能力,面試的時候面試官都喜歡問我們快速排序,那麼用Swift如何實現一個快速排序呢?首先擴充套件Array類:

extension Array {
    var decompose : (head: T, tail: [T])? {
        return (count > 0) ? (self[0], Array(self[1..<count])) : nil
    }
}

屬性decompose的作用是返回陣列中的第一個元素和剩下的元素,注意這個屬性是可選型的,當count為0的時候返回nil,count是Array的屬性。使用擴充套件的原因是這種拆分可以實現非常多的操作,一勞永逸。
然後實現快速排序的方法:

func qsortDemo(input: [Int]) -> [Int] {
    if let (pivot, rest) = input.decompose {
        let lesser = rest.filter { $0 < pivot }
        let greater = rest.filter { $0 >= pivot }
        return qsortDemo(lesser) + [pivot] + qsortDemo(greater)
    } else {
        return []
    }
}

可以發現使用Swift實現快速排序的程式碼非常的簡潔。首先呼叫待排序序列的decompose屬性,使用一個元組來儲存陣列的第一個元素和首先的陣列,由於依舊是採用遞迴的方式,所以使用可選繫結來做邊界判斷。在可選繫結內部使用了filter方法來分割元素,省去了比較移動元素的複雜過程,得到的lesser是小於pivot的陣列、greater是大於pivot的陣列,在返回時使用了陣列的拼接並對拆分的陣列進行遞迴,結構非常的簡單,至此一個快速排序的過程就結束了。
讓我們在storyboard中做個性能測試:

var a:[Int] = [1,2,4,6,2,4,3,7,8]
qsortDemo(a)

陣列a在快排中的效率如下:
這裡寫圖片描述
可以看到可選繫結中的return執行了9次等於a中元素的個數,和預想的一樣,這是因為每一個元素是在這個return中確定自身的位置的,所以執行次數應該為n。那麼為什麼else中的語句執行了n+1次呢?想知道每一個元素在一次遞迴中發生了什麼,可以把讓a中只有一個元素模擬一次遞迴發生的事情,結果如下圖:
這裡寫圖片描述
開啟decompose的執行記錄:
這裡寫圖片描述
可以看到decompose被執行了三次,第一次是[1]來訪問,返回了([1],[]),此時在可選繫結中,lesser和greater都是[],在return中遞迴的時候lesser和greater會繼續訪問decompose此時返回了兩個nil,所以對應的可選繫結判斷為假直接執行else中的return[],整個過程結束。
else條件返回的是[],[]加入到陣列中不會起作用,所以可以作為邊界返回值。
把a中的元素擴充到兩個。
這裡寫圖片描述


decompose中的執行記錄為:
這裡寫圖片描述
很好理解了,第一次拆分得到[1]和[2],pivot為[1],lesser為[],而greater為[2]。在return時lesser訪問decompose得到nil,可選繫結為假執行else中的語句,此時greater又成了一個元素的陣列,步驟同上。所以在這個快排的遞迴過程中每次只有最後一個元素的lesser和greater會同時為[],其他元素都只有一邊為[],這也就解釋了為什麼return會出現n+1的執行次數。
觀察一下兩個filter,這種拆分方法需要多餘的空間來儲存lesser和greater,點開追蹤可以看到lesser和greater中的追蹤軌跡是相反的,這很好理解。另外filter是系統API,並不知道內部的實現方法,但是可以看到在判斷[2]中的元素的時候被呼叫了三次,應該與內部機制有關,雖然看起來執行的次數變多了,但是免去了傳統快速排序中的元素交換位置的操作,效率高低並不好說。總之寫了這麼多最後的效果就一個:排序。
在看完這段程式碼後我做了如下思考:既然是排序,那麼必然可以使用系統的sorted方法(以前的sort方法),效果如何呢?讓我們用第一個例子來試試,只需要一行程式碼:

let b = a.sorted{$0<$1}

效果如何呢?請看下圖:
這裡寫圖片描述
沒錯,整個方法只有15次比較!效率非常的驚人,sorted的實現是由蘋果的工程師在底層實現的,我想他們一定用了什麼好辦法來提升效率。不信?來看下面的例子,我們都知道快速排序的最壞情況出現在遞迴時對陣列的不均衡劃分上,比如修改陣列a為:

var a:[Int] = [1,1,1,1,1,1,1,1,1]

陣列的整體大小沒有發生變化,執行效率如圖:
這裡寫圖片描述
可以看到演算法的主要耗時部分lesser和greater的執行此時由之前的35次變為45次了,那麼sorted方法的執行效率又如何呢?
這裡寫圖片描述
你沒有看錯!對於快排最頭疼的順序性陣列,sorted的重複次數只有n次!說明在面對這種型別的陣列的時候sorted方法進行過判斷,直接輸出了。當然閉包中的語句一定要合適,“千萬不要使用等於號!”,比如改寫a:

var a:[Int] = [1,1,2,2,3,1,1,1,1]

沒有等於號的情況:
這裡寫圖片描述
如果你寫上等於號:
這裡寫圖片描述
OMG!效果一樣的前提下效率差了好多。
另外一種極端情況,完全逆序一個數組:
這裡寫圖片描述
當然快排的時間和完全相同的元素一樣:
這裡寫圖片描述
如果覺得數量級太小不過癮,那麼來個大號的陣列:
現在修改a為500個隨機的100以內的正整數:

var a:[UInt32] = []
for _ in 0..<500{
  a.append(arc4random() % 100)
}

同時比較兩種排序方式,下面是快排的:
這裡寫圖片描述
下面是sorted的效率:
這裡寫圖片描述
大家可以試試,規模越大的陣列效率差別越明顯,sorted以肉眼可見的速度秒殺了快排!
掌聲在哪裡?