Swift90Days - 用函數式編程解決邏輯難題git
這篇翻譯的文章,用兩種方法解決了同一個邏輯難題。第一種方法的編程風格接近大多數 iOS 開發者,實現了指令式編程的解決方案。第二種方法利用了 Swift 的一些語言特性,實現了函數式編程的解決方案。github
源代碼能夠在這裏下載:https://github.com/ijoshsmith/break-a-dollar算法
前陣子朋友和我提及,把1美圓分解成更小的面額,有293種方法。換句話說,若是一個哥們兒告訴你他有1美圓,那麼他的手裏有293種可能的組合,有多是兩個50美分,也多是4個25美分。次日,我就開始嘗試用代碼去解決這個問題。這篇博客回顧了當時想到的兩種解決方案。編程
對於不熟悉美圓硬幣的同窗,能夠先了解一下美圓的硬幣。以下圖所示,1美圓(dollar) = 100美分(cent):swift
思考後我發現用一種比較簡單骯髒的手段解決這個問題並不難,可是這還遠遠不夠。我想找到一種優雅的解決方案,因此我嘗試從各個角度思考這個問題,最終獲得了想要的答案。數組
解決這個問題的關鍵在於遞歸的分解問題。「如何用各類硬幣組合拼成1美圓」,更寬泛點講,其實就是「如何用各類硬幣組合拼成指定金額」。app
舉我的民幣的例子。你欠人家100塊,人家說你100塊都不給我。你說好,我給!因而掏出兩張50,這即是一個50+50的解決方案。
這時你發現有一張是嶄新的50,你不想給他這張50,因而你的問題變成了:如何用手裏的碎錢組合出50面額的錢。
後來你把50換成了5張10塊,這即是一個50+10*5的解決方案,而後感受有一張10塊是嶄新的,要不我換成硬幣給他。
因而問題又變成了:如何組合出10面額的錢。就是這樣慢慢拆分下去。編程語言
點擊 這裏 查看完整的算法回顧。wordpress
我屢次提到「硬幣」這個詞,實際上一枚硬幣也就是一個整數值,代替了它價值多少美分。我寫一個枚舉類存儲全部的硬幣面額,而後再用一個靜態方法降序返回全部的值:函數式編程
enum Coin: Int { case SilverDollar = 100 case HalfDollar = 50 case Quarter = 25 case Dime = 10 case Nickel = 5 case Penny = 1 static func coinsInDescendingOrder() -> [Coin] { return [ Coin.SilverDollar, Coin.HalfDollar, Coin.Quarter, Coin.Dime, Coin.Nickel, Coin.Penny, ] } }
指令式編程的一個重要觀點是:變量改變狀態。指令式的程序像是一種微型控制器,它告訴計算機如何完成任務。接下來的 Swift 代碼你們看起來應該都不陌生,由於 objc 就是一種指令式的編程語言:
func countWaysToBreakAmout(amount: Int, usingCoins coins:[Coin]) -> Int{ let coin = coins[0] if (coin == .Penny) { return 1 } var smallerCoins = [Coin]() for index in 1..<coins.count { smallerCoins.append(coins[index]) } var sum = 0 for coinCount in 0...(amount/coin.rawValue) { let remainingAmount = amount - (coin.rawValue * coinCount) sum += countWaysToBreakAmout(remainingAmount, usingCoins: smallerCoins) } return sum }
仔細看下上面的代碼,計算過程一共分三步:
smallerCoins
) ,存儲比當前硬幣更小的硬幣,用來做爲下次調用的參數。這樣的代碼對於指令式編程來講再日常不過,接下來咱們就來看下如何用函數式編程解決這個問題。
函數式編程的依賴對象,是函數,而不是狀態變化。沒有太多的共享數據,就意味着發生錯誤的可能性更小,須要同步數據的次數也越少。 Swift 中函數已是一等公民,這讓高階函數變成可能,也就是說,一個函數能夠是經過其它函數組裝構成的。隨着 objc 中 block 的引入, iOS 開發者對這個應該並不陌生。
下面是個人函數式解決方案:
func countWaysToBreakAmount(amount: Int, usingCoins coins:Slice<Coin>) -> Int{ let (coin, smallerCoins) = (coins[0], coins[1..<coins.count]) if (coin == .Penny) { return 1 } let coinCounts = [Int](0...amount/coin.rawValue) return coinCounts.reduce(0) { (sum, coinCount) in let remainingAmount = amount - (coin.rawValue * coinCount) return sum + self.countWaysToBreakAmount(remainingAmount, usingCoins: smallerCoins) } }
第二個參數是 Slice<Coin>
而不是數組,由於不必把硬幣拷貝到新的數組裏。咱們只須要用數組的一個切片就能夠,也就是第一行代碼裏的 smallerCoins
,在函數式編程裏稱之爲 tail
。咱們把數據中的第一個元素稱之爲 head
,剩下來的部分稱之爲 tail
。將數組進行切分在下標越界的狀況下也不會引起異常。若是數組中只剩下一個元素,這時 smallerCoins
就爲空。
我用元組的語法同時獲取了 coin
和 smallerCoins
這兩個數據,由於取頭取尾能夠說是同一個操做。與其寫一堆代碼去解釋如何先取出第一個元素,而後再獲取剩下的元素,不如直接用「取出頭部和尾部」這樣語義化的方式一步到位。
接下來,也並無採用循環而後改變局部變量的方法來計算剩餘的組合數,而是用 reduce
這個高階函數。若是你對 reduce
這個函數不太熟悉,能夠看下這篇文章有個大概的瞭解。
首先 coin
指當前處理的硬幣, coinCounts
是一個數組,裏面存儲了全部當前面額的硬幣的可能出現的數目。好比 amount
是10, coin
是3,那麼 coinCounts
的值就是,面額爲3的硬幣可能有多少。顯然應該最多出現3個,因此 coinCounts
是 [1,2,3] 這樣的一列數。而後在分別對每種狀況進行分解計算。
Swift 對於函數式編程的支持讓我感受的興奮,Excited!換種方式思考或許是個不小的挑戰,可是這都是值得的。幾年前我自學了一些 Haskell ,我很欣喜的發現一些函數式思考習慣,讓我在 iOS 開發中也能受益不淺。
示例項目的源代碼能夠在這裏下載。
原文地址: