此文承接上一篇, 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
复制代码