有序數組的一種實現

做者:Ole Begemann,原文連接,原文日期:2017-02-08
譯者:四娘;校對:Cwift;定稿:CMBhtml

上週的 Swift Talk 裏,Florian 和 Chris 編寫了一個有序數組類型 SortedArray:一個老是按照指定規則排序的數組。這很贊,由於它將多個不變性編碼到了類型系統裏。用戶可使用這個類型去取代普通的 數組 ,並且不用擔憂忘記手動排序數組。git

爲了保持視頻簡短,Florian 和 Chris 省略掉了一些很實用的功能。我想給你展現一下這部分實用功能的實現。這些實現都不難編寫,個人主要目的是讓你明白藉助標準庫去實現一個緊貼需求的自定義集合類型是多麼簡單。github

你能夠去 GitHub 上查看所有代碼 ,但下面會有更多講解。算法

指定集合的協議

如視頻所示,SortedArray 遵照了 RandomAccessCollection ,它可讓隨機訪問數組元素的操做變得更快,稍後實現一個高效的 Binary Search 的時候會用到它。express

實現部分很直觀,由於把全部東西都橋接到用來實際存儲的數組那裏。因爲 Index 是 Int 類型,你甚至不用本身實現 index(_:offsetBy:)distance(from:to:) 函數,標準庫已經提供了默認的實現。swift

SortedArray 不能遵照 MutableCollection 或者 RangeReplaceableCollection ,由於他們的 語義 -- 插入/替換特定位置的元素,跟咱們保持元素有序的原則衝突。數組

字面量表達

SortedArray 也不遵照 ExpressibleByArrayLiteral ,即你不能像下面這麼作:app

let sorted: SortedArray = [3,1,2]

這個功能很好,可是你沒辦法給一個字面數組傳遞排序算法,而且 SortedArray 的元素必須遵照 Comparable 。由於 Swift 3 還不支持 conditional protocol conformance ,因此須要寫成下面這樣:dom

extension SortedArray: ExpressibleByArrayLiteral where Element: Comparable {
    ...
}

也許 Swift 4 中能夠實現 conditional protocol conformance。ide

Binary search

使用有序數組的好處之一就是能夠經過 Binary Search 快速找到某一個數組元素。在這裏 Binary Search 的時間複雜度 是 log n 而不是線性的

爲了實現該算法,我首先寫了一個輔助函數 search(for:)。你能夠去 GitHub 上查看完整代碼 ;這裏我想討論一下返回的類型:

fileprivate enum Match<Index: Comparable> {
    case found(at: Index)
    case notFound(insertAt: Index)
}

extension SortedArray {
     
    /// 使用 Binary Search 找到 `newElement`
    ///
    /// - Returns: 若是 `newElement` 在數組裏,就返回 `.found(at: index)`,
    ///   這裏的 `index` 是數組裏元素的位置
    ///   若是 `newElement` 不在這個數組裏,就會返回 `.notfound(insertAt: index)`
    ///   這裏的 `index` 是根據排序算法得出元素應該插入的位置
    ///   若是數組包含了多個元素,且都等於 `newElement`,那就沒法保證哪個會被找到
    ///   
    /// - Complexity: O(_log(n)_),這裏的 _n_ 是數組的大小.
    fileprivate func search(for newElement: Element) -> Match<Index> {
        ...
    }
}

標準庫裏的 index(of:) 返回的是一個 Optional<Index>,沒有找到的狀況就會返回 nil。而 search(for:) 方法也相似,但它的返回值是一個自定義的枚舉,不管是 .found 或者 .notFound 都會帶上一個序號做爲附加信息。這可讓咱們在搜索和插入時使用一致的算法:返回的序號就是咱們須要維持有序數組時插入元素的位置。

算法準備就緒以後,就能夠開始實現 index(of:)contains(_:) 了:

extension SortedArray {

    /// 返回特定值在集合裏第一次出現的位置
    ///
    /// - Complexity: O(_log(n)_),這裏的 _n_ 是數組的大小
    public func index(of element: Element) -> Index? {
        switch search(for: element) {
        case let .found(at: index): return index
        case .notFound(insertAt: _): return nil
        }
    }

    /// 返回一個布爾值,表示這個序列是否包含給定的元素
    ///
    /// - Complexity: O(_log(n)_),_n_ 是數組的大小
    public func contains(_ element: Element) -> Bool {
        return index(of: element) != nil
    }
}

須要注意的是,這裏的實現不止比標準庫裏的實現更高效,並且通用性更強. 標準庫裏這個方法還要求 where Iterator.Element: Comparable 的約束,而 SortedArray 老是擁有一個排序算法,因此不須要這樣的約束。

插入元素

下一個任務是利用 binary search 的優點去提升插入元素的效率。我決定提供兩個插入函數: 第一個會在正確的位置去插入單個元素,保持數組有序。它利用 binary search 去找到正確的插入位置,複雜度爲 O(log n)。插入新元素到非空數組裏,最糟糕的時間複雜度是 O(n),由於所有已有元素不得不移動位置去提供空間。

第二個函數能夠插入一組序列。這裏我選擇先把全部元素都插入到數組最後,而後進行一次從新排序. 這比重複尋找正確的插入位置更快(若是插入的數組元素個數大於 log n)。

extension SortedArray {
    
    /// 插入一個新元素到數組裏,並保持數組有序
    ///
    /// - Returns: 新元素插入的位置
    /// - Complexity: O(_n_)     這裏的 _n_ 是數組大小
    ///    若是新元素插入到數組最後,時間複雜度降低到 O(_log n_)
    @discardableResult
    public mutating func insert(_ newElement: Element) -> Index {
        let index = insertionIndex(for: newElement)
        // 若是元素能夠被插入到數組最後,則複雜度爲 O(1)
        // 最糟糕的狀況是 O(_n) (插入到最前面時)
        _elements.insert(newElement, at: index)
        return index
    }

    /// 插入 `elements` 裏的全部元素到 `self` 裏,保持數組有序
    /// 這會比每一個元素都單獨插入一遍更快
    /// 由於咱們只須要從新排序一次
    ///
    /// - Complexity: O(_n * log(n)_),_n_ 是插入後數組的大小
    public mutating func insert<S: Sequence>(contentsOf newElements: S) where S.Iterator.Element == Element {
        _elements.append(contentsOf: newElements)
        _elements.sort(by: areInIncreasingOrder)
    }
}

其它優點

Chris 和 Florian 已經在這一集裏作過展現,咱們能夠獲得一個更高效的 min() max() 。由於最小值最大值分別是有序集合的第一個和最後一個元素:

extension SortedArray {
     /// 返回集合裏的最小值
    ///
    /// - Complexity: O(1).
    @warn_unqualified_access
    public func min() -> Element? {
        return first
    }

    /// 返回集合裏的最大值
    ///
    /// - Complexity: O(1).

    @warn_unqualified_access
    public func max() -> Element? {
        return last
    }
}

當你在類型的內部實現中調用這些函數,卻沒有顯式地寫明 self. 的前綴時,@warn_unqualified_access 會告訴編譯器拋出一個警告 。這樣能夠幫助你避免混淆了這些函數與全局函數 min(_:_:) max(_:_:)

如同 index(of:)contains(_:) 同樣,咱們的 min()max() 更加通用,由於它們不須要元素是 Comparable 的. 咱們得到了更高的效率,更少的約束。

只有協議要求的才能夠自定義

這四個方法都不是 Sequence 和 Collection 協議裏要求必須實現的,他們不在協議的定義裏。他們只是拓展裏的默認實現。結果就是,調用這些方法的時候都會是靜態派發,由於他們不是 可自定義的

SortedArray 裏的實現並不會重寫默認實現(由於只要協議裏定義的方法才能夠被重寫),他們只是附屬品。當你直接使用 SortedArray 的時候,更加高效的實現會讓你收益。但當它們做爲泛型時將永遠不會被調用。例如:

let numbers = SortedArray(unsorted: [3,2,1])
    // 這會直接調用 SortedArray.max()
    let a = numbers.max()

func myMax<S: Sequence>(_ sequence: S ) -> S.Iterator.Element?
    where S.Iterator.Element: Comparable {
    return sequence.max()
}

// 這種寫法調用的是 Sequence.max() 了(更低效的版本)
let b = myMax(numbers)

咱們沒辦法改變這個 "bug",swift-evolution 有討論過讓這些方法變成協議的一部分(我不肯定這是否是一個好的作法)。

2017.02.09 更新: 我忘了 index(of:)contains(_:) 這些方法,如今還不是 Sequence 和 Collection 的一部分,由於他們須要 Iterator.Element 是 Equatable 的。而如今尚未方法去定義一個泛型協議。Brent Royal-Gordon 在 swift-evolution 裏進行了相關的討論而且提問泛型協議是否應該加入 Swift 裏。

切片

我嘗試着把 SortedArray 保存在一個 ArraySlice 而不是 Array 裏,這麼作的優點就是能夠很是簡單地把 SortedArray.SubSequence 定義爲 ArraySlice。這會讓切片操做變得很是簡單,由於 sortedArray.prefix(5) 會直接返回另外一個 SortedArray,而不是默認的 RandomAccessSlice

最後我仍是決定放棄這種作法,由於長時間持有一個 ArraySlice 的實例不是一件好事. 就算只持有一個很是大的數組的切片,也會一直間接持有那個大的數組,這會致使很是高的內存佔用,這是使用者不想看到的,就算基底 Array 的內存不會泄露,但切片仍是會讓它沒法及時釋放。

外部 Modules 引入的泛型類型的性能表現

若是你想在你的代碼裏使用 SortedArray (或者別的性能要求比較高的泛型),我建議你不要直接把它做爲第三方 module 引入,而是 直接把源代碼文件加入到你的 module 裏

就 Swift 3 而言,Swift 沒法在跨 Module 的狀況下表現出泛型類型的優點。換而言之,若是你在代碼裏使用了 SortedArray<Int>,而且 SortedArray 是定義在另外一個 Module 的時候,編譯器沒法爲元素爲 Int 的 Array 優化生成代碼,只能按照常規的方式,將每個泛型值打包到一個容器裏,而後經過 witness table 進行方法派發。這很容易形成你的代碼在執行時被拖慢 一到兩個數量級

當前版本的 Swift 編譯器沒法約束從外部 Module 引入的泛型(標準庫除外)。…這個限制會讓外部 Module 引入的集合類型性能大幅降低。特別當集合中的元素是簡單的,被極度優化的值類型,例如 Int,甚至是 String。 依賴引入的 Module,你的集合裝填基礎數據類型時性能會有 10-200 倍的降低。

標準庫是惟一一個例外,標準庫裏的類型對於任何 Module 都是可見的。

我但願 Swift 編譯器團隊能夠找到一個方法解決這個問題。雖然我不知道該怎麼作。編譯器如今加入了一個
非正式的修飾符 @_specialize (可能在 以後會加入一個新的語法 )。給一個方法添加這個修飾符時,相關的類型就告訴編譯器爲本身生成特殊的代碼。目前正在開發的版本里,這個修飾符好像支持使用 _Trivial64 去把全部不那麼重要的值類型都封裝成相同的大小。

總結

完整的實現 總共兩百多行,包括註釋。

就像你看到的,自定義集合類型有不少須要考量的東西。並且咱們都只考慮了接口的設計,咱們甚至還沒接觸底層的實現。但我以爲這些付出都是有回報的。咱們得到了一個行爲和內建集合類型徹底一致的類型,兼容序列和集合操做的同時還會根據算法自我變化。

雖然跨 Module 使用泛型類型確實對於性能有很大的影響。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索