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