此文承接上一篇, Session 連接git
首先咱們按 PPT 拆解下代碼:github
extension MutableCollection {
/// Moves all elements satisfying `isSuffixElement` into a suffix of the collection,
/// returning the start position of the resulting suffix.
///
/// - Complexity: O(n) where n is the number of elements.
mutating func halfStablePartition(isSuffixElement: (Element) -> Bool) -> Index {
guard var i = firstIndex(where: isSuffixElement) else { return endIndex }
var j = index(after: i)
while j != endIndex {
if !isSuffixElement(self[j]) { swapAt(i, j); formIndex(after: &i) }
formIndex(after: &j)
}
return i
}
}
extension MutableCollection where Self : RangeReplaceableCollection {
/// Removes all elements satisfying `shouldRemove`.
/// ...
/// - Complexity: O(n) where n is the number of elements.
mutating func removeAll(where shouldRemove: (Element)->Bool) {
let suffixStart = halfStablePartition(isSuffixElement: shouldRemove)
removeSubrange(suffixStart...)
}
}
複製代碼
咱們找一個例子走一遍過程(選出全部負數並刪除):算法
[1, 2, -1, -2, 3, 4, -3, -4, -5, 5] // 初始
[1, 2, -1, -2, 3, 4, -3, -4, -5, 5] // i == 2, j == 3
[1, 2, 3, -2, -1, 4, -3, -4, -5, 5] // i == 2, j == 4
[1, 2, 3, -2, -1, 4, -3, -4, -5, 5] // i == 3, j == 5
[1, 2, 3, 4, -1, -2, -3, -4, -5, 5] // i == 4, j == 6
[1, 2, 3, 4, -1, -2, -3, -4, -5, 5] // i == 4, j == 7
[1, 2, 3, 4, -1, -2, -3, -4, -5, 5] // i == 4, j == 8
[1, 2, 3, 4, -1, -2, -3, -4, -5, 5] // i == 4, j == 9
[1, 2, 3, 4, 5, -2, -3, -4, -5, -1] // i == 5, j == endIndex
[1, 2, 3, 4, 5] // 刪除右邊部分
複製代碼
上述算法中 i 和 j 都是順序遍歷, 一般狀況下 j 會比 i 前進的快些(j 每次都會自增), 總的複雜度爲 O(n).swift
halfStablePartition
方法的主要做用是按 isSuffixElement
條件將數組分爲左右兩個部分, 左邊是不知足條件的部分, 右邊是知足條件的部分, 並返回右邊部分的起始下標.數組
而後經過 removeSubrange
方法將右邊部分所有刪除, 這樣就實現了 removeAll
.數據結構
這個算法的巧妙之處在於, 左邊部分不影響在原有數組中的相對順序, 右邊部分雖然順序有變可是由於隨後會被刪除, 因此不受影響.app
到這裏你們可能會以爲作些解法都有點繞, 直接用額外的數組存一下, 或者使用 filter
方法是否是更直接些? 可是這兩種方法會用到額外的存儲空間.數據結構和算法
正看成者準備背起書包回家的時候, "老學究"問他"難道項目中沒有相似問題了麼?" 其實對比咱們本身每每也是這樣的, 解決完一個 bug 就大功告成了, 至於還有其餘地方須要優化, 有空再說吧.編輯器
而後做者放下書包開始繼續查看代碼. 代碼寫習慣了, 類似的錯誤可能會被帶到項目的各個角落, 下面感覺下相似錯誤的地方:ide
我先看第一個, 將選中的圖形都移到前面, 並保持相對順序不變:
extension Canvas {
mutating func bringToFront() {
var i = 0, j = 0
while i < shapes.count {
if shapes[i].isSelected {
let selected = shapes.remove(at: i)
i += 1
shapes.insert(selected, at: j)
j += 1
}
}
}
}
複製代碼
查一下文檔 remove
和 insert
都是 O(n) 複雜度的操做, 合起來仍是 O(n), 再加上 while 循環, 又是一個 O(n²) 複雜度的算法.
那麼看一下優化後的代碼:
extension Canvas {
/// Moves the selected shapes to the front, maintaining their relative order.
mutating func bringToFront() {
shapes.stablePartition(isSuffixElement: { !$0.isSelected })
}
}
複製代碼
其中 stablePartition
的實現能夠在這個連接中找到, 咱們留到最後進行分析.
這個算法的含義是按條件 isSuffixElement
進行分類, 知足條件的放在後面, 不知足條件的放在前面, 算法複雜度爲O(n log n).
既然是 bringToFront
那麼就是沒有選中的放後面, 因此條件就是 !$0.isSelected
.
同理咱們能夠實現一個 sendToBack
方法, 即選中的放後面, 因此條件就是 $0.isSelected
:
extension Canvas {
/// Moves the selected shapes to the back, maintaining their relative order.
mutating func sendToBack() {
shapes.stablePartition(isSuffixElement: { $0.isSelected })
}
}
複製代碼
咱們來看一下另外一個方法 bringForward
, 這個方法的做用是將選中的全部元素統一插入到選中的第一個元素的前一個位置並保持相對順序不變.
調用方法以前:
調用方法以後:
咱們仍是先看一下修改前的代碼:
extension Canvas {
mutating func bringForward() {
for i in shapes.indices where shapes[i].isSelected {
if i == 0 { return }
var insertionPoint = i - 1
for j in i..<shapes.count where shapes[j].isSelected {
let x = shapes.remove(at: j)
shapes.insert(x, at: insertionPoint)
insertionPoint += 1
}
return
}
}
}
複製代碼
這裏雖然是兩層 for 循環, 可是這兩個循環是先後銜接的關係, 因此仍是 O(n) 的複雜度, 總的複雜度仍是 O(n²).
到這裏你或許會問, 那這個算法和 stablePartition
方法有什麼聯繫呢? 這裏做者給了咱們一個提示, 若是咱們把選中的第一個元素的前一個位置做爲分割點把數組分爲左右兩個子數組, 而後對右邊的子數組作 stablePartition
是否是就能夠了? 那麼這個算法的複雜度就能夠優化到 O(n log n) 了.
分割示意圖:
修改後的代碼:
extension Canvas {
mutating func bringForward() {
if let i = shapes.firstIndex(where: { $0.isSelected }) {
if i == 0 { return }
let predecessor = i - 1
shapes[predecessor...].stablePartition(isSuffixElement: { !$0.isSelected })
}
}
}
複製代碼
這裏我對解題思路有一個反思. 做者是怎麼一步一步聯想到這些解題步驟的呢? 難道是僅僅是他本身設計了這個演講的緣由麼?
ArraySlice
和 stablePartition
這些數據結構和算法算法優化暫告一段落, 做者作了一下延伸, 咱們怎麼去測試咱們的代碼? 難道是要在 Canvas App 上手動建立一堆圖形, 而後手動選擇圖形, 點擊對應的操做按鈕肉眼看一下效果麼? 其實這個正是咱們開發 App 的時候最經常使用且最原始的 debug 方式, 得益於 Xcode 模擬器超快的啓動速度, 因此不少開發人員直接修改代碼, run 起來看一下效果, 不行就改一下再 run, 或者加一些 log, 或者斷點調試下. 做爲 App 開發人員不多會去思考對本身的算法作單元測試.
對本身代碼作單元測試的這個習慣我是後面重構遺留代碼的時候才養成的, 再後來開始作 SDK 的相關開發, 更加意識到單元測試的重要性.
既然上述寫法並不利於單測, 那麼怎麼去修改呢?
Canvas
這個類)從代碼的通用性和複用性來說第二種方式比較好, 這裏做者就是朝這個方向去改寫代碼的.
首先咱們想到的是, 既然不依賴於 Canvas
這個類, 並且這個算法的整個功能實際上是對 Array
的操做, 那麼是否是能夠抽取到 Array
的 extension
裏面去呢? 咱們看一下修改後的代碼:
extension Array where Element == Shape {
mutating func bringForward() {
if let i = firstIndex(where: { $0.isSelected }) {
if i == 0 { return }
let predecessor = i - 1
self[predecessor...].stablePartition(isSuffixElement: { !$0.isSelected })
}
}
}
複製代碼
可是你會發現, 雖然作了抽取, 可是這個 extension
依然依賴於 Shape
類, 解耦的還不完全, 因此進行第二次修改:
extension Array {
mutating func bringForward(elementsSatisfying predicate: (Element) -> Bool) {
if let i = firstIndex(where: predicate) {
if i == 0 { return }
let predecessor = i - 1
self[predecessor...].stablePartition(isSuffixElement: { !predicate($0) })
}
}
}
複製代碼
這裏修改的地方涉及到兩個:
where Element == Shape
中去除對於 Shape
類的依賴$0.isSelected
中將判斷條件由外面傳參進來(由於 isSelected
是 Shape
類特有的), 使算法更通用既然說到了更爲通用, 那麼這個算法僅僅只適用於 Array
麼? 是否是 MutableCollection
都適用呢? 想一想挺有道理, 因而修改代碼變成 extension MutableCollection
試試, 可是編輯器直接報錯了.
由於 MutableCollection
的 index
並不是是 Int
類型的, 不能直接和 0 比較, 或者進行減 1 操做. 第一直覺是改爲這樣 extension MutableCollection where Index == Int
, 做者提醒 "Don't do this.". 這樣又算法進行特殊化了, 變的不夠通用了.
其實若是是個人話, 修改到 extension Array
已經以爲能夠了, 已經足夠通用且可單元測試, 畢竟這個算法在 App 中也是給 Array
使用的.
"老學究"幾個直擊靈魂的提問, 令人有更進一步的想法. 若是咱們不糾結於 "和 0 比較, 進行減 1 操做" 等細節問題, 將問題進一步抽象化, 思考下這兩行代碼的做用是什麼呢? 選中的第一個元素的前一個位置 -- indexBeforeFirst
. 那麼抽象後的代碼:
extension MutableCollection {
mutating func bringForward(elementsSatisfying predicate: (Element) -> Bool) {
if let predecessor = indexBeforeFirst(where: predicate) {
self[predecessor...].stablePartition(isSuffixElement: { !predicate($0) })
}
}
}
複製代碼
而後再來具體看下 indexBeforeFirst
的實現:
extension Collection {
func indexBeforeFirst(where predicate: (Element) -> Bool) -> Index? {
return indices.first {
let successor = index(after: $0)
return successor != endIndex && predicate(self[successor])
}
}
}
複製代碼
適當的抽象可以簡化問題, 也可以將問題拆解而後進行聚焦.
最後要加上必要的文檔, 完美. 你會問本身給本身寫的接口也須要文檔麼? 那麼回去看一下半年前寫過的超過100行的沒有註釋的一段代碼, 還記得是幹啥的麼? 清晰的文檔, 於人於己都是方便, 特別在大廠你的代碼後續確定由別人一塊兒維護, 爲了減小 WTF 的數量, 建議仍是寫上 ^.^ .
整個優化工做並無完成, 做者放出了最後一段待優化的代碼, 這段代碼的做用的是將選中的元素聚焦於選擇的位置:
extension Canvas {
mutating func gatherSelected(at target: Int) {
var buffer: [Shape] = []
var insertionPoint = target
var i = 0
while i < insertionPoint {
if shapes[i].isSelected {
let x = shapes.remove(at: i)
buffer.append(x)
insertionPoint -= 1
}
else {
i += 1
}
}
while i < shapes.count {
if shapes[i].isSelected {
let x = shapes.remove(at: i)
buffer.append(x)
}
else {
i += 1
}
}
shapes.insert(contentsOf: buffer, at: insertionPoint)
}
}
複製代碼
受前面 bringForward
方法的啓發, 咱們在選擇的位置處將數組分爲左右兩個部分, 左邊部分將選中元素後置, 右邊部分將選中元素前置, 這樣總的算法複雜度仍是 O(n log n):
extension MutableCollection {
/// Gathers elements satisfying `predicate` at `target`, preserving their relative order. ///
/// - Complexity: O(n log n) where n is the number of elements.
mutating func gather(at target: Index, allSatisfying predicate: (Element)->Bool) {
self[..<target].stablePartition(isSuffixElement: predicate)
self[target...].stablePartition(isSuffixElement: { !predicate($0) })
}
}
extension Canvas {
mutating func gatherSelected(at target: Int) {
shapes.gather(at: target) { $0.isSelected }
}
}
複製代碼
最後咱們來分析下 stablePartition
算法:
extension MutableCollection {
/// Moves all elements satisfying `isSuffixElement` into a suffix of the
/// collection, preserving their relative order, and returns the start of the
/// resulting suffix.
///
/// - Complexity: O(n) where n is the number of elements.
/// - Precondition: `n == self.count`
mutating func stablePartition(count n: Int, isSuffixElement: (Element) -> Bool) -> Index {
if n == 0 { return startIndex }
if n == 1 { return isSuffixElement(self[startIndex]) ? startIndex : endIndex }
let h = n / 2, i = index(startIndex, offsetBy: h)
let j = try self[..<i].stablePartition(count: h, isSuffixElement: isSuffixElement)
let k = try self[i...].stablePartition(count: n - h, isSuffixElement: isSuffixElement)
return self[j..<k].rotate(shiftingToStart: i)
}
}
複製代碼
這裏用到了遞歸+旋轉的方式.
用例子來看一下:
7, 6, -7, -6, 5, 4, -5, -4, -3, 3, 2, -2, -1, 1
i
7, 6, -7, -6, 5, 4, -5 | -4, -3, 3, 2, -2, -1, 1
j i k
7, 6, 5, 4 | -7, -6, -5 | 3, 2, 1 | -4, -3, -2, -1
| rotate |
7, 6, 5, 4 | 3, 2, 1 | -7, -6, -5 | -4, -3, -2, -1
7, 6, 5, 4, 3, 2, 1, -7, -6, -5, -4, -3, -2, -1
複製代碼