做者:Jacob Bandes-Storch,原文连接,原文日期:2015/08/05
译者:Lou;校对:shanks;定稿:shankshtml
这篇博文启发自Code Review.SE上的一个讨论,同时nerd-sniped上的关于数学的有趣的学习。让我对数学和 Swift 的结合有了兴趣。因此我花了一段时间来把这些知识整理成一篇博文,特别是自从我完成了对我网站重建的第一步之后。更重要的是,我但愿我能更勤勉的更新个人博客,这8年我只写了一篇而已,但愿你们能对个人博客感兴趣。
这篇博文的目标对于初学者来说,比较容易理解,同时也提供给那些已经对这个概念熟悉的人一些有用的细节和例子。但愿你们能给我反馈。ios
假设你第一次学习 Swift,你实在是太兴奋了,花了一天时间反复练习,等到次日就成了专家。因而次日你就开始传授课程来教别人。git
固然,我很愿意成为你的第一个学生。我也学的很快,一天学下来,我也能够教别人 Swift 了。我俩继续教别人,其余的学生也学的很快,立刻跟上进度,均可以次日就去教别人。github
这是个多么让人兴奋的世界呀。可是问题来了,照这样的进度下去,Swift 学习者将大量涌入城市,基础设施将没法支撑庞大的人口。swift
市长叫来最好的科学家们:“咱们须要精确的数学模型!天天到底有多少人会使用 Swift?何时这种疯狂会终止?”数组
为了方便理解问题,让咱们画一副图来表示最初几天发生的事:安全
仔细观察咱们发现,特定的一天总的 Swifters 数量(咱们用 \\(S_{今天}\\) 来表示)等于前一天的数量加上每一个老师能够所教的学生。性能优化
$$ S_{今天} = S_{昨天} + 老师数 $$数据结构
那么老师数目是多少呢?记住,一我的须要花一天时间学习才能变成 Swift 专家,因此前天的每个人都能成为老师,均可以教一个学生:\\(S_{今天} = S_{昨天} + S_{前天}\\)。闭包
这下公式就简单了!咱们能够用手算了:
0 + 1 = 1 1 + 1 = 2 1 + 2 = 3 2 + 3 = 5 3 + 5 = 8 ...
若是这个数列看上去有点熟悉,那是由于这是斐波纳契数列。
1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,...
无论你是否喜欢,咱们的世界里到处都有斐波那契数的存在:花瓣的生长遵循斐波那契数列,大树的枝丫是斐波那契树丫,固然也有人吐槽说这不过是确认偏误罢了。咱们发现,这个数列是基于很是简单的形式的,很是容易计算:
var i = 0 var j = 1 while true { (i, j) = (j, i + j) print(i) // 打印1, 而后打印1, 继续打印2, 3, 5, 8, 13, 21, 34, 55... }
大功告成!
哈哈,骗你的。咱们才刚刚开始。计算机美妙的地方就在于能够帮助咱们快速的解决用手算很麻烦的问题。让咱们尝试几个例子。
前面咱们已经差很少解决了这个问题,只要在42那边中止循环便可。
var i = 0 var j = 1 for _ in 0..<42 { (i, j) = (j, i + j) } i // returns 267914296
和以前的问题相似,咱们能够将其抽象成一个函数。用 n 来代替 42。
func nthFibonacci(n: Int) -> Int { var i = 0 var j = 1 for _ in 0..<n { (i, j) = (j, i + j) } return i } nthFibonacci(42) // 返回 267914296 nthFibonacci(64) // 返回 10610209857723
为了简化问题,假定每一个人写代码的速度是同样的。知道每一个人天天写的代码量后,咱们只须要把斐波那契数加起来便可。
func fibonacciSumUpTo(n: Int) -> Int { var sum = 0 for i in 0..<n { sum += nthFibonacci(i) // 第 i 天 使用 Swift 写代码的人数 } return sum } fibonacciSumUpTo(7) // 返回 33
不要急,Swift 的标准库里面已经有了一个函数叫作 reduce,能够将数字加在一块儿。咱们该怎么写呢?
[1, 1, 2, 3, 5, 8, 13].reduce(0, combine: +) // 返回 33
这样可行,可是咱们须要把每一个数字都写出来。要是能用 nthFibonacci() 就行了。
既然这些是连续的斐波那契数,咱们能够简单的使用1到7的范围:
[1, 2, 3, 4, 5, 6, 7].map(nthFibonacci) // 返回 [1, 1, 2, 3, 5, 8, 13] [1, 2, 3, 4, 5, 6, 7].map(nthFibonacci).reduce(0, combine: +) // 返回 33
或者咱们能够更简单,用 Swift 的range operator(...):
(1...7).map(nthFibonacci).reduce(0, combine: +) // 返回 33
这等同于 fibonacciSumUpTo
看上去很不错,可是不要忘了 nthFibonacci(i) 从0开始加到 i,所需的工做量将随着i线性增长。
并且咱们所写的 (1...n).map(nthFibonacci).reduce(0, combine: +)
从1到n每次凑要运行 nthFibonacci, 这将大大增长运算量。
注意:计算越简单的斐波那契数,真实耗费每一步的时间几乎能够忽略不计(开启性能优化)。这篇文章以前的草稿版本包括了时间消耗的表格,可是我把表格去掉了,怕误导你们。取而代之的是,咱们讨论的是一个相对的时间/性能的复杂度。
让咱们将 nthFibonacci
和 fibonacciSumUpTo
两个函数结合来减小一点运算量:
func fastFibonacciSumUpTo(n: Int) -> Int { var sum = 0 var i = 0 var j = 1 for _ in 0..<n { (i, j) = (j, i + j) // 计算下一个数 sum += i // 更新总数 } return sum } fastFibonacciSumUpTo(7) // 返回 33
如今咱们已经将 fastFibonacciSumUpTo
的复杂度从二次降为线性了。
可是为了实现这个,咱们不得不写了一个更加复杂的方程。咱们在分离相关度(把计算斐波那契数和求和分为2步) 和优化性能之间进行了权衡。
咱们的计划是用 Swift 的标准库来简化和解开咱们的代码。首先咱们来总结一些咱们要作什么。
将前n个斐波那契数用线性时间(linear time)和常量空间(constant space)的方式加起来。
将前n个斐波那契数用线性时间(linear time)和常量空间(constant space)的方式加起来。
将前n个斐波那契数用线性时间(linear time)和常量空间(constant space)的方式加起来。
幸运的是,Swift 正好有咱们须要的功能!
一、 reduce
函数,用 +
操做符来结合。
二、 prefix
函数和惰性求值(Lazy Evaluation)
注意:prefix只有在 Xcode 7 beta 4中可用,做为 CollectionTypes 的一个全局函数使用,但其实已经在 OS X 10.11 beta 5 API 做为 SequenceType 的扩展出现了。我指望在下一个 Xcode beta 有一个延迟实现的版本;如今这里有一个自定义的实现。
三、 定制数列,使用数列型协议(SequenceType protocol)
Swift 的 for-in
循环的基础是 SequenceType
协议。全部遵循这个协议的能够循环。
想要成为一个 SequenceType 只有一个要求,就是提供一个建立器( Generator
):
protocol SequenceType { typealias Generator: GeneratorType func generate() -> Generator }
而成为一个 GeneratorType
只有一个要求,就是生产元素( Elements
)
protocol GeneratorType { typealias Element mutating func next() -> Element? }
因此一个数列就是一个能够提供元素建立器的东西。
最快建立定制数列的方法就是用AnySequence
。这是一个内建的结构体,能够响应generate()
,去调用一个你在初始化时所给的闭包。
struct AnySequence<Element>: SequenceType { init<G: GeneratorType where G.Element == Element> (_ makeUnderlyingGenerator: () -> G) }
相似的,咱们能够用 AnyGenerator
和 anyGenerator
函数来造建立器。
func anyGenerator<Element>(body: () -> Element?) -> AnyGenerator<Element>
因此写一个斐波那契数列就至关简单了:
let fibonacciNumbers = AnySequence { () -> AnyGenerator<Int> in // 为了建立一个生成器,咱们首先须要创建一些状态... var i = 0 var j = 1 return anyGenerator { // ... 而后生成器进行改变 // 调用 next() 一次获取每一项 // (代码看起来是否是很熟悉?) (i, j) = (j, i + j) return i } }
如今 fibonacciNumbers
是一个 SequenceType
,咱们可使用 for
循环:
for f in fibonacciNumbers { print(f) // 打印 1, 而后打印 1, 继续打印 2, 3, 5, 8, 13, 21, 34, 55... }
并且咱们能够自由的使用 prefix
:
for f in fibonacciNumbers.prefix(7) { print(f) // 打印 1, 1, 2, 3, 5, 8, 13, 而后中止. }
最后咱们能够用 reduce
来加起来:
fibonacciNumbers.prefix(7).reduce(0, combine: +) // 返回 33
太棒了!这是线性时间的,常量空间的,最重要的是这很是清晰的展现了咱们所要作的,而不须要使用 ...
和 map
。
说明:若是你在playground里运行这段代码,可能会发现这个版本比以前的要慢。这个版本只改变了常数部分,复杂度自己没有变化,可是性能却有明显降低。和 fastFibonacciSumUpTo 进行对比能够发现,这段代码把单一的循环改为了函数调用,这可能就是性能下降的缘由。没错,咱们又须要进行权衡。
目前的目标只是给了咱们一个更好给工具去解答有关斐波那契数的问题。深刻钻研来看,咱们可能会问:为何我要先研究斐波那契数?这不过是这个数列刚好符合咱们所发现的规律:
$$S_{今天} = S_{昨天} + S_{前天}$$
这个公式在咱们代码中表现为 (i, j) = (j, i + j)
。可是这深藏了 AnySequence
和 anyGenerator
。若是咱们要写更加清晰的代码 --- 能够描述咱们想要解决的问题、不须要仔细分析 --- 咱们最好写的更加明显点。
斐波那契数列常写成这种形式:
$$F_{n} = F_{n-1} + F_{n-2}$$
这是相似的形式,可是最重要的是这表现出递推关系。这种数学关系指的是数列里某一个数的值取决于前面几个数的值。
定义递推关系的时候,首先要定义初始项。咱们不能简单的利用 (i, j) = (j, i + j)
来计算斐波那契数若是咱们不知道什么是 i 什么是 j。在咱们的例子里,咱们的初始项为 i = 0
和 j = 1
—— 或者,咱们能够把初始值定为1和1,由于咱们是等第一个值返回之后才进行计算的。
递推关系的阶数(order)是指每一步所需的前面项的个数,并且初始项数目必须等于阶数(否则的话咱们就没有足够的信息来计算下一项)。
如今咱们能够来设计API了!你只需提供初始项和递推就能够建立递推关系了:
struct RecurrenceRelation<Element> { /// - Parameter initialTerms: The first terms of the sequence. /// The `count` of this array is /// the **order** of the recurrence. /// - Parameter recurrence: Produces the `n`th term from the previous terms. /// - 参数 initialTerms: 序列的第一个元素集合. /// 数组的个数也就表明这个递推的排序。 /// - 参数 recurrence:根据前面的元素推算出第 n 个元素 init(_ initialTerms: [Element], _ recurrence: (T: UnsafePointer<Element>, n: Int) -> Element) }
(咱们在使用 UnsafePointer<Element>
而不是 [Element]
,这样咱们就可使用 T[n]
而不须要存储先前计算的项)。
如今,咱们的初始任务变得更加简单了。多少人在使用Swift? 只要用这个公式便可:
let peopleWritingSwift = RecurrenceRelation([1, 1]) { T, n in T[n-1] + T[n-2] } peopleWritingSwift.prefix(7).reduce(0, combine: +) // 返回 33
咱们来作吧。
struct RecurrenceRelation<Element>: SequenceType, GeneratorType {
首先咱们须要一些内存来存储元素,还须要一个引用来连接到咱们所要传递的闭包。
private let recurrence: (T: UnsafePointer<Element>, n: Int) -> Element private var storage: [Element] /// - 参数 initialTerms: 序列的第一个元素集合. /// 数组的个数也就表明这个递推的排序。 /// - 参数 recurrence:根据前面的元素推算出第 n 个元素 init(_ initialTerms: [Element], _ recurrence: (T: UnsafePointer<Element>, n: Int) -> Element) { self.recurrence = recurrence storage = initialTerms }
为了简单点,咱们同时采用 SequenceType
and GeneratorType
。对于 generate()
,咱们只返回 self
。
// SequenceType requirement func generate() -> RecurrenceRelation<Element> { return self }
接下来,每次调用 next()
,咱们调用 recurrence
来产生下一个值, 而且将其存在 storage
里。
// GeneratorType requirement private var iteration = 0 mutating func next() -> Element? { // 首先推算出全部的初始元素值 if iteration < storage.count { return storage[iteration++] } let newValue = storage.withUnsafeBufferPointer { buf in // 调用闭包,传入内存地址中的指针的偏移量,知道 T[n-1] 是数组中最后一个元素 return recurrence(T: buf.baseAddress + storage.count - iteration, n: iteration) } // 存储下一个的值,丢弃到最旧的值 storage.removeAtIndex(0) storage.append(newValue) iteration++ return newValue } }
更新:@oisdk指出
UnsafePointer
不是必须的。在原来的版本中,我使用它是为了让 n 的值在 recurrence 中更加精确 - 可是自从 recurrence 只依赖与前一项,而不是 n 自己时,n 的值再也不改变时,这是ok的。 因此这个版本运行良好。不使用UnsafePointer
感受更加安全了!
记住:有许多种方法能够定义自定义数列。CollectionType
,SequenceType
,和 GeneratorType
只是协议,你能够按照本身所需的方式来遵循它们。也就是说,在实践中也许你不多须要这么作 —— Swift 的标准库里有大多数你所需的。不过若是你以为须要自定义的数据结构,你可使用 CollectionType
和 SequenceType
。
如今咱们已经概括了递推关系,咱们能够轻松地计算许多东西了。好比说卢卡斯数(Lucas Number)。和斐波那契数相似,只不过初始项不一样:
// 2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123, 199, 322, 521... let lucasNumbers = RecurrenceRelation([2, 1]) { T, n in T[n-1] + T[n-2] }
或者”Tribonacci Numbers“,一个拥有有趣性质的三阶递推:
// 1, 1, 2, 4, 7, 13, 24, 44, 81, 149, 274, 504... let tribonacciNumbers = RecurrenceRelation([1, 1, 2]) { T, n in T[n-1] + T[n-2] + T[n-3] }
花一些额外的功夫,咱们能够视觉化单峰映像的混沌二根分支。
func logisticMap(r: Double) -> RecurrenceRelation<Double> { return RecurrenceRelation([0.5]) { x, n in r * x[n-1] * (1 - x[n-1]) } } for r in stride(from: 2.5, to: 4, by: 0.005) { var map = logisticMap(r) for _ in 1...50 { map.next() } // 处理一些获得的值 Array(map.prefix(10))[Int(arc4random_uniform(10))] // 随机选择接下来 10 个值当中的一个 }
是否是颇有数学的简洁性呀?
TED 演讲,The magic of Fibonacci numbers, 演讲者,Arthur Benjamin.
Binet's Formula, 使用一个几乎常量时间的公式来计算斐波那契数。
Arrays, Linked Lists, and Performance,做者 Airspeed Velocity, 对序列使用其余有意思的方法,包括对ManagedBuffer的讨论。
本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg。