此文承接上一篇, Session 链接

算法分析

首先我们按 PPT 拆解下代码:

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).

halfStablePartition 方法的主要作用是按 isSuffixElement 条件将数组分为左右两个部分, 左边是不满足条件的部分, 右边是满足条件的部分, 并返回右边部分的起始下标.

然后通过 removeSubrange 方法将右边部分全部删除, 这样就实现了 removeAll.

这个算法的巧妙之处在于, 左边部分不影响在原有数组中的相对顺序, 右边部分虽然顺序有变但是因为随后会被删除, 所以不受影响.

到这里大家可能会觉得做些解法都有点绕, 直接用额外的数组存一下, 或者使用 filter 方法是不是更直接些? 但是这两种方法会用到额外的存储空间.

举一反三

正当作者准备背起书包回家的时候, "老学究"问他"难道项目中没有类似问题了么?" 其实对比我们自己往往也是这样的, 解决完一个 bug 就大功告成了, 至于还有其他地方需要优化, 有空再说吧.

然后作者放下书包开始继续查看代码. 代码写习惯了, 相似的错误可能会被带到项目的各个角落, 下面感受下类似错误的地方:

我先看第一个, 将选中的图形都移到前面, 并保持相对顺序不变:

extension Canvas {mutating func bringToFront() {var i = 0, j = 0while i < shapes.count {if shapes[i].isSelected {let selected = shapes.remove(at: i)i += 1shapes.insert(selected, at: j)j += 1}}}
}
复制代码

查一下文档 removeinsert 都是 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 - 1for 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 - 1shapes[predecessor...].stablePartition(isSuffixElement: { !$0.isSelected })}}
}
复制代码

这里我对解题思路有一个反思. 作者是怎么一步一步联想到这些解题步骤的呢? 难道是仅仅是他自己设计了这个演讲的原因么?

  • "No Raw Loops", 不是优先想到直接用循环去解决这些问题, 而是思考下标准库是否已经提供了类似的解决方案?
  • 对标准库要熟稔于心, 这样才能够第一时间想到 ArraySlicestablePartition 这些数据结构和算法
  • 有一颗 Clean Code 的心, 不断的完善自己的代码

算法优化暂告一段落, 作者做了一下延伸, 我们怎么去测试我们的代码? 难道是要在 Canvas App 上手动创建一堆图形, 然后手动选择图形, 点击对应的操作按钮肉眼看一下效果么? 其实这个正是我们开发 App 的时候最常用且最原始的 debug 方式, 得益于 Xcode 模拟器超快的启动速度, 所以很多开发人员直接修改代码, run 起来看一下效果, 不行就改一下再 run, 或者加一些 log, 或者断点调试下. 作为 App 开发人员很少会去思考对自己的算法做单元测试.

对自己代码做单元测试的这个习惯我是后面重构遗留代码的时候才养成的, 再后来开始做 SDK 的相关开发, 更加意识到单元测试的重要性.

既然上述写法并不利于单测, 那么怎么去修改呢?

  1. 采用 UI 测试替代单元测试, 通过 Mock 数据配合宿主 App 把上述手动流程自动化, 这样可以减少手工操作
  2. 改写上述算法, 将其变的可单元测试. 那么怎么改写? 主要原则就是减少耦合(这里就是不依赖于 Canvas 这个类)

从代码的通用性和复用性来讲第二种方式比较好, 这里作者就是朝这个方向去改写代码的.

首先我们想到的是, 既然不依赖于 Canvas 这个类, 而且这个算法的整个功能其实是对 Array 的操作, 那么是不是可以抽取到 Arrayextension 里面去呢? 我们看一下修改后的代码:

extension Array where Element == Shape {mutating func bringForward() {if let i = firstIndex(where: { $0.isSelected }) {if i == 0 { return }let predecessor = i - 1self[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 - 1self[predecessor...].stablePartition(isSuffixElement: { !predicate($0) })}}
}
复制代码

这里修改的地方涉及到两个:

  1. where Element == Shape 中去除对于 Shape 类的依赖
  2. $0.isSelected 中将判断条件由外面传参进来(因为 isSelectedShape 类特有的), 使算法更通用

既然说到了更为通用, 那么这个算法仅仅只适用于 Array 么? 是不是 MutableCollection 都适用呢? 想想挺有道理, 于是修改代码变成 extension MutableCollection 试试, 但是编辑器直接报错了.

因为 MutableCollectionindex 并非是 Int 类型的, 不能直接和 0 比较, 或者进行减 1 操作. 第一直觉是改成这样 extension MutableCollection where Index == Int, 作者提醒 "Don't do this.". 这样又算法进行特殊化了, 变的不够通用了.

其实如果是我的话, 修改到 extension Array 已经觉得可以了, 已经足够通用且可单元测试, 毕竟这个算法在 App 中也是给 Array 使用的.

Building Towers Of Abstraction

"老学究"几个直击灵魂的提问, 使人有更进一步的想法. 如果我们不纠结于 "和 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 = targetvar i = 0while 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 }}
}
复制代码

算法分析2

最后我们来分析下 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, 1i7, 6, -7, -6, 5, 4, -5 | -4, -3, 3, 2, -2, -1, 1j            i         k7, 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, -17, 6, 5, 4, 3, 2, 1, -7, -6, -5, -4, -3, -2, -1
复制代码

WWDC 2018: Embracing Algorithms(2)相关推荐

  1. WWDC 2018: Embracing Algorithm (1)

    Session 链接 前言 作为一个 iOS App 开发人员, 经常会听到这样的吐槽, 做一个 App 主要是 UI 布局和动画, 平时基本上都用不到算法, 为啥面试的时候总喜欢考算法? 自己也有过 ...

  2. (WWDC) 拥抱算法 (Embracing Algorithms) 下

    在阅读此文前,请先阅读 拥抱算法 (Embracing Algorithms) 上 . 在数组中移动元素 将图层移动到最前面 如果你要将图中选中的图形移动到最前面,你会如何编写你的逻辑代码? 将元素移 ...

  3. (WWDC) 拥抱算法 (Embracing Algorithms) 上

    什么是算法? Algorithm 算法: 在计算或其他解决问题的操作中要遵循的一个或一组规则,特别是通过计算机:一种基本的划分算法. 现在,请假设你正在构建这样一个App: 示例App 你可以选中画布 ...

  4. WWDC 2018:iOS 内存深入研究

    WWDC 2018 Session 416:iOS Memory Deep Dive 查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录 作者:高老师 ...

  5. WWDC 2018: Shortcuts 快速入门

    Shortcuts WWDC 2018 Session 216:Introduction to Siri Shortcuts 查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG ...

  6. 6 月 4 日!苹果 WWDC 2018 或重点推出 iOS 12 系统

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 今日凌晨,苹果正式宣布第 29 届年度全球开发者大会(WWDC 2018)将于美国当地时间 ...

  7. 5分钟带你看完 WWDC 2018

    前言 一年一度的 WWDC(苹果全球开发者大会)于北京时间 6月5日 凌晨1点开幕.废话不多说,来看看这次WWDC 都有哪些亮点吧! iOS 12 和 ARKit 2.0 关键词:官方防沉迷最为致命 ...

  8. 理解崩溃和崩溃日志(WWDC 2018 session 414)

    WWDC 2018 session 414: Understanding Crashes and Crash Logs 每个人在写代码的时候,或多或少都会犯错.有的错误就会导致程序崩溃,这非常影响用户 ...

  9. [译] WWDC 2018:关于iOS 12、iPad Pro、新MacBook及更多产品的所有预测

    原文地址:WWDC 2018: All the rumors on iOS 12, iPad Pro, new MacBooks and more 原文作者:JOHN FALCONE, JUSTIN ...

最新文章

  1. 矩阵连乘算法代码JAVA_矩阵连乘问题的动态规划算法(java)
  2. boost::qvm::deduce_scalar相关的测试程序
  3. 后端技术:Nginx + Spring Boot 实现负载均衡
  4. Vijos——T 1629 八
  5. 数据库基础知识——存储过程和函数
  6. 人工智能相关领域的国际顶尖会议介绍
  7. Unity 全面理解加载和内存管理
  8. 实时数据处理环境搭建flume+kafka+storm:4.storm安装配置
  9. 一个nginx 502问题解决方案
  10. 架构 什么是CDN加速?为什么要用CDN
  11. java创建ssh链接池_HttpClient实战三:Spring整合HttpClient连接池
  12. 基于C#实现的简易绘图工具设计
  13. Windows的Git Bash使用tree命令
  14. 仿QQ局域网聊天软件
  15. 极域课堂管理软件(V2016)遇到的问题——“未找到可用的网络适配器,将以脱机模式启动”
  16. 老师用计算机教我们画画拼音,小学一年级语文《汉语拼音13angengingong》第三课时教学设计.docx...
  17. 蓝桥杯——调手表(python解答)
  18. 这也能卖?拉美电商平台Mercado Libre上的10种奇葩产品
  19. uniapp 使用支付宝扫码插件
  20. 【苹果推】imessage群发条件文档(推送证书)开发工具Pushnotification

热门文章

  1. NAL header
  2. 云南省8市、8洲行政区划 (2023)
  3. android lut格式文件,LUT 文件扩展名: 它是什么以及如何打开它?
  4. Oracle 通过15位或18位身份证号码计算年龄、出生年月日、性别
  5. 4.4 51单片机-NEC红外线遥控器解码
  6. 震惊!某知名网站sojson.com站长竟为别墅抛弃百万群友,至群友无家可归
  7. D. Armchairs(dp)
  8. 医疗机器人软件中的机器人协作技术:机器人技术在医疗保健中的应用
  9. 因果推断、强化学习与理性人工智能奇点
  10. 软件公司创业完全手册[一]