WWDC 2018: Embracing Algorithm (1)

Session 連接git

前言

做爲一個 iOS App 開發人員, 常常會聽到這樣的吐槽, 作一個 App 主要是 UI 佈局和動畫, 平時基本上都用不到算法, 爲啥面試的時候總喜歡考算法?程序員

本身也有過這樣的疑惑, 項目中確實不多使用到算法, 通常就是經常使用的幾種設計模式用熟, MVC 和 MVVM 選一個, 而後就開始各類第三方庫, 難一點的可能會遇到一些多線程的問題或者組件化開發?github

放出 PPT 裏面的一張圖感覺下, 確實很好的總結了 iOS App 開發的精髓.面試

App架構

可是看過這個 Session 以後, 算是獲得一些啓示, 一個程序員的自我修養最終仍是繞不過算法, 代碼的優雅和高效始終是咱們所追求的, 這無關乎業務和麪試.算法

介紹演講者

以爲有必要介紹下演講者swift

WWDC 2015 時候第一次看到 Dave Abrahams 的演講, 當時講的是這個 Session "Protocol-Oriented Programming in Swift".設計模式

在維基百科上搜了下, Dave Abrahams 以前就已是 C++ STL 的貢獻者之一了, 13年加入 Apple 在 Swift 的核心庫小組裏面擔任 TL. 因此這篇演講也引用了一些 C++ STL 中的哲學思想.數組

Dave Abrahams 的演講方式也挺有意思的, 採用一種自編自導自演的方式, 創造了一個苛刻的老學究的角色, 模擬對話, 而後引出演講的主題. 將本該嚴謹死板的算法講出了一些趣味和發人深省的地方.多線程

演講源碼

除了 Session 視頻, PPT 固然也是要下載的, 得益於 Swift 的開源, PPT 中的代碼實現均可以在 GitHub 上找到.架構

GitHub地址: https://github.com/apple/swift, 固然 master 分支上面是沒有的, 切到 swift-4.2-branch-06-11-2018 分支而後在 swift/stdlib/public/core/RangeReplaceableCollection.swift 文件裏面能夠找到 removeAll 方法, 裏面就是 PPT 中講到的實現.

可是比較奇怪的是在 swift-4.2-branch 分支上面這個實現已經變了, 估計 Apple 的開發人員一直在優化這塊的實現, 畢竟4.2目前還不是穩定版本.

一個 Bug 引發的思考

Session 以一個圖形 App 做爲例子, 看一下這個 App:

App

而後引出一個 bug, 這個 bug 也是咱們新手開發 App 的時候比較容易犯錯的一個問題, 對於數組邊遍歷邊刪除的問題.

看一下問題:

p-1

如圖, 圖中有10個圖形, 其中咱們選中第8個將其刪除, 可是刪除的時候 crash 了, why?

看一下問題代碼:

extension Canvas {
    mutating func deleteSelection() {
        for i in 0..<shapes.count {
            if shapes[i].isSelected {
                shapes.remove(at: i)
            }
        }
    }
}
複製代碼

p-2

遍歷的範圍 0..<shapes.count 一開始就已經肯定了(10個元素), 當遍歷到第8個圖形的時候, 發現其被選中則進行 remove 操做, 後面兩個元素往前補位, 這個時候數組裏面只有9個元素了, 因此再按照最開始的範圍遍歷到第十個元素時組數越界產生 crash.

由於平時使用 Objective-C 比較多, 咱們結合 Objective-C 來看看, 咱們熟悉的數組遍歷方式有:

  1. 普通 for 循環遍歷
for (NSInteger i = 0; i < shapes.count; i++) {
        // do something
    }
複製代碼
  1. for-in 遍歷 (這種方式在邊遍歷邊刪除的時候會拋異常).
for (Shape *shape in shapes) {
        // do something
    }
複製代碼
  1. block 枚舉遍歷
[shapes enumerateObjectsUsingBlock:^(Shape * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        // do something
    }];
複製代碼

以上除了第二種方式會拋異常之外, 1和3這兩種都能"混"過去, 爲何是"混", 咱們來分析下, 假設這個 bug 中的第9個元素也是被選中的, 那麼當遍歷到第8個圖形的時候, 發現其被選中則進行 remove 操做, 後面兩個元素往前補位, 可是此時下標並無處理, 下一次遍歷會直接從第9個元素開始(也就是原先的第10個元素), 從而把原生的第9個元素直接跳過去了, 出現了漏刪除的行爲.

此類問題我出過一個面試題, 面試題不是很難, 有近一半的面試者出現過邊遍歷邊刪除的問題(爲啥出這個題, 由於我也是踩過坑的~).

好了回到正題上, 那麼緣由找到了, 具體怎麼個解法呢? Session 中還繞了好幾個彎, 咱們先來看第一個彎:

extension Canvas {
    mutating func deleteSelection() {
        var i = 0
        while i < shapes.count {
            if shapes[i].isSelected {
                shapes.remove(at: i)
            }
            i += 1
        }
    }
}
複製代碼

這個改法和普通的 for 循環相似, 只是改爲了 while 循環, 問題也比較明顯, 假設若是第9個元素也一樣被選中, 就會存在漏刪的問題, 緣由上面已經分析過了.

既然是由於下標沒有處理, 那麼處理下下標不就能夠了? 第二個彎:

extension Canvas {
    mutating func deleteSelection() {
        var i = 0
        while i < shapes.count {
            if shapes[i].isSelected {
                shapes.remove(at: i)
            }
            else {
                i += 1
            }
        }
    }
}
複製代碼

這個解法是可行的, 還有別的解法麼? 逆向思惟下, 既然刪除一個元素以後, 後面的元素是往前進行補位的, 這樣影響的是正序遍歷時候的下標. 若是咱們採用逆序遍歷是否是就不存在這個問題了? 第三個彎:

extension Canvas {
    mutating func deleteSelection() {
        for i in (0..<shapes.count).reversed() {
            if shapes[i].isSelected {
                shapes.remove(at: i)
            }
        }
    }
}
複製代碼

其實咱們通常修改 bug 的話至此就已經完事了, 甚至連逆向思考一下可能都不會去想, 其實這只是剛剛開始.

鼠標移到 remove 方法, 按住 option 鍵而後點擊查看下文檔, remove 方法竟然是個 O(n) 複雜度的操做. 再加上外層的 while 循環, 整個方法的複雜度有O(n²), 看到這裏我也吃了一驚.

後面, 做者給咱們科普了下算法的複雜度還有 Mac 上字典中對於算法的定義. 應該也是做爲一個引子吧.

這個時候已經不是在解 bug 了, 上升了一個層次 - 代碼優化, 先放代碼:

extension Canvas {
    mutating func deleteSelection() {
        shapes.removeAll(where: { $0.isSelected })
    }
}
複製代碼

代碼精簡了不少, 語義也十分清晰, 這裏多了個 removeAll 方法, 這個方法應該是 Swift 4.2 新的方法, 以前的版本並無找到這個方法. 固然整個過程是值得咱們學習的, 對於咱們後續封裝本身的擴展方法也是頗有啓發的.

若是你裝了 Xcode 10 能夠點開 removeAll 的文檔看一下, 複雜度爲 O(n), 這裏是否是勾起了你的好奇心, 從 O(n²) -> O(n) 這個是怎麼辦到的? 若是是你本身優化了這個解法, 是否是這一成天都是神清氣爽的.

extension RangeReplaceableCollection where Self: MutableCollection {
  /// Removes all the elements that satisfy the given predicate.
  ///
  /// Use this method to remove every element in a collection that meets
  /// particular criteria. This example removes all the odd values from an
  /// array of numbers:
  ///
  /// var numbers = [5, 6, 7, 8, 9, 10, 11]
  /// numbers.removeAll(where: { $0 % 2 == 1 })
  /// // numbers == [6, 8, 10]
  ///
  /// - Parameter predicate: A closure that takes an element of the
  /// sequence as its argument and returns a Boolean value indicating
  /// whether the element should be removed from the collection.
  ///
  /// - Complexity: O(*n*), where *n* is the length of the collection.
  @inlinable
  public mutating func removeAll( where predicate: (Element) throws -> Bool
  ) rethrows {
    if var i = try firstIndex(where: predicate) {
      var j = index(after: i)
      while j != endIndex {
        if try !predicate(self[j]) {
          swapAt(i, j)
          formIndex(after: &i)
        }
        formIndex(after: &j)
      }
      removeSubrange(i...)
    }
  }
}
複製代碼

上半部分就先講到這, 下半部分還會用到這個算法, 到時候詳細闡述下.

最後放上一句 PPT 中的至理箴言 "No Raw Loops". 怎麼作到這一點? 那就是對 Swift 標準庫要作到如數家珍.

相關文章
相關標籤/搜索