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 标准库要作到如数家珍.

相关文章
相关标签/搜索