有序数组的一种实现

做者: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

相关文章
相关标签/搜索