有人對 Swift/SwiftUI 程式設計有興趣嗎?

#12. 第3課 自訂新指令:函式(func)

以Swift設計的程式,基本上可以簡化為「資料」與「指令」所構成,前兩課的範例中,我們用 let, var 來定義資料,用等號 = 來指定資料值,用 + - * / 來做資料運算;用 print() 指令來輸出字串,用 for, while 等迴圈指令來重複執行程式片段,資料與指令的適當組合,以完成某些工作,就構成一個程式。

我們會學到很多Swift預先設計好的指令,除此之外,我們還可以自行定義新的指令,也就是新的函式。函式原文是 function,跟數學的函數是同一個字,意思也大同小異,函式或函數都需要傳入參數,然後經過一段運算,得到一個結果。

例如我們第2課教過的1到100的整數和,如果我們想設計一個函式來計算1到任意數n 的整數和,該如何做呢?

// 1-3a 函式(func): 1到任意數n的整數和
// Created by Heman, 2021/07/09

func 整數和(n: Int) -> Int {
var 累計值 = 0
for i in 1...n {
累計值 = 累計值 + i
}
return 累計值
}

let x = 整數和(n: 1001)
print(x)

1-3a 範例中,我們定義了一個新函式:整數和(),接受一個參數名稱為 n 的整數,計算結果會傳回一個整數,函式本體 { } 裡面的程式碼,則幾乎跟範例1-2a一模一樣,只是將100改成可變的參數n。

所以定義函式的語法類似這樣:

func 整數和(n: Int) -> Int { }


通用的語法像這樣:
func 函式名稱(參數1: 資料類型, 參數2: 資料類型, ...) -> 回傳值的資料類型 { }


函式可以接受多個參數或完全不要參數,視需要來定義,如果完全不要參數,小括號 () 還是不可少,在上例中我們只需要一個參數。

當使用函式的時候(稱為函式呼叫 "Function Call"),只要在「函式名稱()」填入參數名稱與參數值,就會得到計算結果(回傳值),例如:

let x = 整數和(n: 1001)


令 x 等於 1 到 1001 的整數和。

什麼時候需要自訂函式?在第2課,我們只想要算1到100的整數和,因為這是高斯的經典問題,是特例,到了本課,我們想要推廣求1到任意數的整數和,變成通用的算法,所以函式可看成一種通用化的工具。

不過,如果參數只有一個的時候,呼叫時還要寫出參數名稱有點笨,下面例子提供一個偷吃步。

// 1-3b 函式(func): 參數名稱省略
// Created by Heman, 2021/07/09

func 整數和(_ n: Int) -> Int {
var 累計值 = 0
for i in 1...n {
累計值 = 累計值 + i
}
return 累計值
}

let x = 整數和(1001)
print(x)

函式宣告時,參數名稱前面的底線符號 _,具有「可省略」的意思,函式可以有一個或多個可省略參數(呼叫時不寫參數名稱),但必須寫在一般參數之前。這樣,函式呼叫的時候,從 整數和(n: 1001) 改成 整數和(1001) 是不是稍微順眼一點?

我們還可以進一步再推廣這個函式的用途:求 m 到 n 的整數和。

// 1-3c 函式(func): 求m到n整數和
// Created by Heman, 2021/07/09

func 整數和(m: Int = 1, _ n: Int) -> Int {
var 累計值 = 0
for i in m...n {
累計值 = 累計值 + i
}
return 累計值
}

let x = 整數和(1001)
let y = 整數和(m: 101, 1001)
print(x, y)

注意 func 定義時,參數名稱 m 額外加了一個「預設值」m: Int = 1,也就是說,定義函式時,參數若指定預設值的話,函式呼叫時,該參數就可以整個省略,例如 x = 整數和(1001),這時候 m 就會用預設值,計算1到1001的整數和。

函式的參數預設值沒有限制數量,可以每個參數都有預設值,純粹視需要而定。

當然,若要計算中間一段,如101到1001的整數和,就呼叫「整數和(m: 101, 1001)」即可。所以相當於這個函式有兩種呼叫方法,這樣通用性就更好了。
哇現在才看到
從第一課開始學
謝謝老師分享
#13. 為什麼不直接套用公式?

前面所舉程式範例,包括整數和、三角形面積,甚至多項式的微積分,都有已知公式,何不直接套用公式?當然可以,如果是數學考試,熟背公式、套用公式是絕對必要的,但在此是為了學習迴圈,不用公式更好。

當我們學一個新技能、新工具時,應該先拿新工具來解已知問題,這樣一方面能逐漸熟悉新工具,另一方面,用新工具得出來的結果,可以用舊方法來加以驗證,就知道我們學得對不對。而新工具,往往威力更強大,才值得學習。

而我們學習的過程都是從簡單到複雜,從特定解法推廣到通用解法,從已知領域探索到未知領域,所以不要看到熟悉問題就說:「這個簡單,我會了,跳過去!」用不熟悉的新工具,去解決未知的問題,絕對不是個好方法。

學習程式設計跟數學稍有不同,因為我們人腦無法像電腦重複大量計算,所以善用數學公式簡化問題對人腦很有幫助。而電腦可以大量運算,所以程式設計盡量學習通用的解法,而不是特定問題的快速解法。套用公式是針對已知問題,比較特定,用迴圈則是通用解法,遇到沒有公式的問題也能適用。

很多已知問題,上網Google一下都可找到答案,透過網路分享,我們能快速累積知識,只要有人遇過類似問題,就有可能在網路上找到程式碼,這也是為什麼要學習通用解法,因為大部分已知的問題,別人都幫你做好,只要會呼叫函式就行,我們自己要解決的,通常是獨特的問題,也就是自己創新的地方。

無論如何,紮實的基礎很重要,把程式設計的基礎學好,以後才有創新的機會。如果沒有基礎,甚至連抄襲模仿的機會都沒有。
#14. 人生的「變數」

人生有很多難以預期的變數,有些變數是機會,有些變數是風險,或讓一帆風順的生活瞬間破碎,或在痛苦掙扎中帶來曙光,一個變數就能改變一生的軌跡。

1979年冬天,一位24歲年輕人走進史丹佛大學附近一間頂尖的研究中心,這次拜訪改變了他的一生,甚至也改變了整個世界。

這位只唸過大學一學期的年輕人,在三年前跟一位朋友創立一家小公司,設計一台手工拼裝、可以編寫程式的8位元計算機,頗受年輕玩家喜愛,經過三年努力,產品已銷售全美國,準備在明(1980)年股票上市,在上市前被邀到這家大公司,大老闆想利用上市前投資一輪,以便上市後賺一票。

年輕人說可以,只有一個條件,他知道這公司的研究中心常發表最新科技,希望帶工程師團隊在研究中心待三天做技術交流,大老闆說當然沒問題。隔年年輕人公司股票順利上市,大老闆出清股票獲利數百萬美金(哇,那三天的代價太划算了!)。

這個研究中心叫 Xerox PARC,是1938年發明乾式影印技術的全錄(Xerox)公司,獨佔影印機市場鰲頭數十年後,在1969年大手筆投資創立的,網羅全美最頂尖的計算機人才。在這個地方,年輕人看到許多令人眼睛一亮的新技術,都是 Xerox PARC 過去十年的研發成果:
  • Alto 全新概念的個人用電腦
  • Alto Exec 第一套圖形介面(GUI)的作業系統
  • Mouse 第一套滑鼠
  • Smalltalk 第一個物件導向(OOP)的程式語言
  • Ethernet 區域網路(LAN)技術
  • Bravo 所見即所得(WYSIWYG)的編輯軟體
  • Laurel 電子郵件(e-mail)軟體
  • Laser Printer 雷射印表機
雖然只有短短三天的技術交流,年輕人回去後興奮不已,決定要開發出類似的系統,與整個團隊夜以繼日全力研發,並且利用股票上市獲得的資金,挖角研究中心多位科學家,在1983年推出市場上第一套具有圖形介面的個人電腦,叫做Lisa,可惜售價太高、bug太多,市場冷淡導致公司損失慘重,但年輕人不屈不撓,經過一年改頭換面,1984年再推出 Macintosh (Mac) 終於大獲成功。

這個年輕人叫做 Steve Jobs,在1976年與Steve Wozniak創立了Apple公司。

我們現在學習的Swift程式語言,完全是物件導向,很大部分是繼承過去30年蘋果公司的官方語言Objective-C,而 Objective-C 則是融合 Smalltalk 與 C 語言的特色,這與 Mac電腦、macOS作業系統,都是1979年那次拜訪的人生變數所帶來的影響。

至於原創者,全錄公司的 Alto 電腦,只生產了原型機,從未上市,錯失了整個世代的個人電腦革命以及龐大市場。

Steve Jobs visits Xerox Palo Alto Research Center in 1979 (取自電視影集)


註解
  1. 1980年12月12日蘋果電腦公司(當時名為Apple Computer Inc)股票在納斯達克(NASDAQ)證券交易所,以每股22美元上市,當天收盤價漲到29美元。
  2. 通常股票上市前最後一輪增資,每股價格會低於未來公開上市(IPO)的發行價格,是增資過程中,風險最低且幾乎保證獲利,所以是投資人的最愛。
#15. 第4課 臨兵鬥者皆陣列在前:陣列(Array)

商朝是中國歷史上,第一個留下文字記載的朝代,在1899年首次發現甲骨文之後,考古學家與當地村民在河南安陽小屯村陸續挖掘出十幾萬片帶文字的龜甲獸骨,雖然都是盤庚遷殷之後所留下來的,但從記載內容可以上溯第一代帝王商湯及先祖,成為中國最早有考古證據的可信歷史。

從甲骨文紀錄結合史記與竹書紀年加以推測,商朝從西元前1558年成湯即位到西元前1046年紂王覆滅,共30位帝王,歷時512年。那個時代充滿殺伐征戰與血腥祭祀,部落之間經常掠奪人口為奴隸,一次征戰可出動數十部落、數千士兵,可以想像戰場上那種「臨兵鬥者皆陣列在前」的肅殺氣氛,令人不寒而慄。

商朝也是一個擅長貿易的國家,貿易範圍遠超過其邊界,從考古證據中,可以看到來自新疆的和田玉,有來自南海的大龜甲,東海的精緻海貝(還可當作貿易的貨幣),各地來的犀角象牙等等,所以「商人」成為從事貿易者的代名詞,流傳至今。

在程式設計裡面,「陣列(Array)」是用來處理大量同類資料的好方法,就像士兵列陣,排成「一字長蛇陣」,每個士兵都有編號,一個陣列視為一個整體(指定為一個變數或常數)。

陣列是一種衍生的資料類型,由一組同類型的資料集合而成,每個資料稱為陣列的「元素」。我們用商朝的帝王世系來寫個範例程式,陣列元素由字串構成。
// 1-4a 陣列(Array)
// Created by Heman, 2021/07/12

let 商王世系: [String] = [
"大乙湯", "外丙勝", "仲壬庸", "大甲至",
"沃丁絢", "大庚辯", "小甲高", "大戊密",
"雍己伷", "中丁莊", "外壬發", "河亶甲整",
"祖乙滕", "祖辛旦", "沃甲踰", "祖丁新",
"南庚更", "陽甲和", "盤庚旬", "小辛頌",
"小乙斂", "武丁昭", "祖庚曜", "祖甲載",
"廩辛先", "庚丁囂", "武乙瞿", "文丁托",
"帝乙羨", "帝辛紂" ]

print(商王世系[0])
print(商王世系[29])
print(商王世系[30])

for i in 0...29 {
print(商王世系[i])
}

for 商王 in 商王世系 {
print(商王)
}

在1-4a範例程式中,用 [String] 來宣告新的資料類型「字串陣列」,是由字串String同類資料所構成的陣列。
let 商王世系: [String] 

定義一個常數「商王世系」,屬 [String] 資料類型,也就是「字串陣列」類型,然後給初始值,內容為30個商王的名號。

要取用陣列中的個別元素,可用「商王世系[i]」,其中 i 稱為陣列的索引(index),就像士兵的編號,從 0 開始算,「商王世系[0]」可取得陣列第一個商王的名號(資料值)。

所以上例中,最後(第30個)元素索引為29,也就是「商王世系[29]」。如果不小心用「商王世系[30]」就會出現錯誤,無法再執行下去(若是App會閃退)。所以必須將這行刪除或"Remark"起來,才能執行下去。

我們可以用 for 迴圈將整個世系印出來,有兩種方法,第一是利用索引值 0...29 來取用個別元素。
for i in 0...29 {
print(商王世系[i])
}

第二種是 for 迴圈的新用法,但是更好更簡便,因為我們不需要知道索引值的範圍,就可以將陣列全部掃描一遍,這也是更通用的做法。其中可以看出,for迴圈的「迴圈參數」不必局限於整數,可以是任何資料類型,在此例中,迴圈參數「商王」就屬於「字串」類型。
for 商王 in 商王世系 {
print(商王)
}

#16. 商王寶藏

自1899(光緒25)年北京國子監祭酒王懿榮無意中發現甲骨文之後,並未引起政府重視,直到將近30年後,國民政府成立中央研究院歷史語言研究所,才終於在1928年由董作賓等人到河南安陽小屯村正式展開考古挖掘,持續到1937年抗戰爆發為止,共進行15次挖掘,發現兩萬多片甲骨與數十件國寶及珍貴文物。原來,小屯村附近就是盤庚遷殷後的都城「殷」,從盤庚到紂王歷經12位帝王、273年。

1934年冬天,在小屯村附近王陵區發現一座完整的小墓,裡面有10件青銅器,如今都被列為國寶,其中的「三節提梁卣(卣唸有,酒器)」非常特別,一套三件,含酒壺、酒杯、蓋子,造型優美極具巧思,充分展現商人精湛的鑄造技藝。

這些國寶並不是收藏在外雙溪故宮博物院,而是保存在南港的中央研究院。


三節提梁卣 http://museum.sinica.edu.tw/collection/7/item/77/

我們在程式1-4a列出商王世系的名號,接下來我們加入每位商王即位的年代,逐漸完善商王世系的資料。

// 1-4b 商王年表
// Created by Heman, 2021/07/14

let 商王世系: [String] = [
"大乙湯", "外丙勝", "仲壬庸", "大甲至",
"沃丁絢", "大庚辯", "小甲高", "大戊密",
"雍己伷", "中丁莊", "外壬發", "河亶甲整",
"祖乙滕", "祖辛旦", "沃甲踰", "祖丁新",
"南庚更", "陽甲和", "盤庚旬", "小辛頌",
"小乙斂", "武丁昭", "祖庚曜", "祖甲載",
"廩辛先", "庚丁囂", "武乙瞿", "文丁托",
"帝乙羨", "帝辛紂" ]

let 商王即位年表: [Int] = [
-1558, -1546, -1544, -1540,
-1528, -1509, -1504, -1487,
-1412, -1400, -1391, -1381,
-1372, -1353, -1339, -1334,
-1325, -1319, -1315, -1287,
-1284, -1274, -1215, -1204,
-1171, -1167, -1159, -1124,
-1111, -1102 ]

for i in 0...29 {
print(商王世系[i], 商王即位年表[i])
}




在這裡我們遇到兩個小問題:

(1) 要將帝王名號與即位年代在同一行列印出來,似乎無法用 for 商王 in 商王世系 { } 這樣的通用句法。
(2) 執行結果,年代顯示負整數似乎不合習慣,應該印出「西元前XXXX年」較好。

還好,在我們學完下面幾課之後,這兩個問題就能夠順利解決。
#17 第5課 是非題 Bool 與選擇題 if-else

在上一課範例1-4b 列印商王年表時,我們注意到西元-1558年應該表達為「西元前1558年」較合乎習慣,這需要一個 if 指令去判斷年份,如果年份 n小於0,就印出「西元前xxxx年」,如果 n大於0,就印出「西元xxxx年」。用Swift的寫法如下:
if n < 0 {
print("西元前\(abs(n))年")
} else {
print("西元\(n)年")
}

這整個句子稱為「if-else 條件句」或簡稱「if 條件句」,一般的 if 條件句,else 後半段如果不需要是可以省略的,不過在這裏是需要的。

還記得字串裡面倒斜線 \() 的特殊用法嗎?這種用法非常重要,如果忘記,請參考第2課(#40樓)內容。

另外,如果 n小於0,需要取 n的絕對值,可呼叫另外一個函式 abs(n),傳回 n的絕對值,abs() 即 "absolute"(絕對值)的簡寫。

if 後面的 "n < 0" 稱為「邏輯運算式」,和之前的算術運算式類似,也會得到一個結果值,算術運算式的結果可能是整數或實數,而邏輯運算式的結果只有兩個值:真(true)或假(false),true, false 這兩個值(注意沒有引號 " ")組成一個新的資料類型,稱為 Bool (布爾或布林)類型。

如果 if 後面的邏輯運算式結果為 true,則執行緊接的 { } 段落,否則為 false 就執行 else 之後的 { } 段落。

這裡還有一個很方便也很常用的特殊用法:
if n < 0 {
print("西元前\(abs(n))年")
} else {
print("西元\(n)年")
}
可以寫成
print(n < 0 ? "西元前\(abs(n))年" : "西元\(n)年")

注意其中
n < 0 ? "西元前\(abs(n))年" : "西元\(n)年"

這是一個用問號 ? 與冒號 : 組成的混合運算式,如果 n < 0 則取前面 "西元前\(abs(n))年" 字串,否則取冒號後面 "西元\(n)年" 字串,這種用法可以取代簡單的 if條件句。

所以,我們可以修改商王年表的程式,印出西元前的年代。下面範例程式中,我們用上面兩種的寫法,分別寫成兩個函式「西元()」與「西元年()」,功能是一模一樣,兩種語法都必須熟悉。

// 1-5a 商王年表(if)
// Created by Heman, 2021/07/15

let 商王世系: [String] = [
"大乙湯", "外丙勝", "仲壬庸", "大甲至",
"沃丁絢", "大庚辯", "小甲高", "大戊密",
"雍己伷", "中丁莊", "外壬發", "河亶甲整",
"祖乙滕", "祖辛旦", "沃甲踰", "祖丁新",
"南庚更", "陽甲和", "盤庚旬", "小辛頌",
"小乙斂", "武丁昭", "祖庚曜", "祖甲載",
"廩辛先", "庚丁囂", "武乙瞿", "文丁托",
"帝乙羨", "帝辛紂" ]

let 商王即位年表: [Int] = [
-1558, -1546, -1544, -1540,
-1528, -1509, -1504, -1487,
-1412, -1400, -1391, -1381,
-1372, -1353, -1339, -1334,
-1325, -1319, -1315, -1287,
-1284, -1274, -1215, -1204,
-1171, -1167, -1159, -1124,
-1111, -1102 ]

func 西元(_ n: Int) -> String {
if n < 0 {
return "西元前\(abs(n))年"
} else {
return "西元\(n)年"
}
}

func 西元年(_ n: Int) -> String {
let 字串 = n < 0 ? "西元前\(abs(n))年" : "西元\(n)年"
return 字串
}

for i in 0...29 {
print(商王世系[i], 西元(商王即位年表[i]))
}

print("----- ----------")
for i in 0...29 {
print(商王世系[i], 西元年(商王即位年表[i]))
}

函式 func 西元年(_ n: Int) -> String { } 參數前面加上底線 _,代表參數名稱可省略,直接寫參數值即可,如果忘了,請回頭參考第3課函式。


if 條件句與布爾類型對初學者來說,可能是一個較大的關卡,要跨過去才能走得更遠。其實每一個條件句,就是程式的一個轉折點,很多條件組合下來,程式就有很多轉折,所以條件句是讓程式產生千變萬化的主要原因。

另外,程式如果「語法錯誤」,Swift Playgrounds 會檢查出來,提示你修改;但是如果「邏輯錯誤」,Swift Playgrounds 是不會知道的,很多App執行時的bug都是邏輯錯誤造成,所以我們在寫條件句的時候,要特別用心檢查,記得先測試。

註解
  1. Bool 這個名稱是為了紀念二百年前的英國數學家喬治布爾(George Boole, 1815-1864),他發明了一個數學分支,稱為布林代數(Boolean Algebra),可別小看只有 true, false 兩種值,布林代數和二進位算術可說是現代半導體與電腦的數學基礎,沒有布林代數和二進位,就不可能有電腦、手機或任何電子產品。
  2. 二進位算術是三百年前德國數學家萊布尼茲(Leibniz, 1646-1716)所發明,每個位元(bit)值為0或1,疊加之後,可以代表所有數字,例如十進位的13可以轉換為二進位1101(4位元)。二進位的「位元數」越多,就能表示越大或越精確的數字,64位元電腦指的就是以64個位元為基本計算單位的電腦。

#18. 尋找質數

大家在學校都學過質數,所謂「質數」就是「一個大於1的正整數,除了1與本身之外,沒有其他的因數」,就稱為質數,最小的質數是2。

質數在網路時代有個很重要的應用,就是加密的通訊協定"HTTPS","HTTPS" 用在幾乎所有網站或App中,加密的原理就是先找尋兩個「非常大」的質數當作秘密鑰匙,通常要「數百位數」這麼大的質數,然後將這兩個大質數相乘的乘積傳出去,如果第三者想看到秘密,必須將乘積做因數分解,設法獲得那兩個大質數,但這會讓電腦花很長很長時間還無法解開。

所以質數可說是我們網路私密的守門員。我們就來設計一個簡單的程式,找出1000以內(三位數以下)的質數。

// 1-5b 尋找質數
// Created by Heman, 2020/07/08
func 是質數嗎(_ n: Int) -> Bool {
if n < 2 {
return false
} else if n == 2 {
return true
}
for i in 2...(n-1) {
if (n % i) == 0 {
return false
}
}
return true
}

var 計數 = 0
let 上限 = 1000
for i in 1...上限 {
if 是質數嗎(i) {
計數 = 計數 + 1
print("#\(計數).", i)
}
}
print("小於\(上限)的質數一共\(計數)個")

我們先寫一個函式「是質數嗎(n)」,用來判斷 n 是不是質數,若是則傳回 true,不是則傳回 false,所以函式回傳的類型是 Bool。

函式中,主要判斷質數的原理很簡單,就是試試看n 是否能被小於 n且大於等於2的數整除(即餘數為0)。

在這裡有個新的運算符號,百分比 % ,用來計算兩數相除的餘數,函式裡面:
 if (n % i) == 0 { } 

其中 (n % i) == 0 就是指「n 除以 i 所得的餘數是否等於0」,也就是「n 能否被 i 整除」。記得前面提過,Swift 程式裡面,等號 = 是用來指定值給變數,若要判斷兩數是否相等,要用兩個等號 == 。除了 == 之外,用來比較的邏輯運算符號包括 !=, <=, >=, <, >,這些邏輯運算符號同樣可以用在「字串」或其他類型的比較,以後遇到再說。

 a == b  a, b兩數是否相等
a != b a, b兩數是否不相等
a <= b a是否小於或等於b
a >= b a是否大於或等於b
a < b a是否小於b
a > b a是否大於b

有了「是質數嗎(n)」這個函式,尋找1000以下的質數就很簡單,用一個 for 迴圈即可。如果要找1百萬以下的質數,也只要程式修改「上限」值即可。

註解
目前已知最大質數超過「2千萬位數」,不是2千萬喔,是「2千萬位數」,1後面接2千萬個0。 https://en.wikipedia.org/wiki/Largest_known_prime_number
#19. 道法自然:費氏數列

老子的道德經提到:「人法地,地法天,天法道,道法自然」,大自然中有無數值得探索的奧秘,一向是哲學與科學的泉源。

1968年基隆一位漁民在東沙群島附近捕撈到一隻從未看過的大螺,直徑比成人手掌還大,帶回基隆請人鑑定,竟然是非常稀有的活化石:龍宮翁戎螺,轟動全台。

這種貝類是五億年前孑遺至今的物種,全世界只發現過三隻標本,更別說是活體,沒幾天日本人聞風趕來,以一萬美金高價買回日本,一萬美金在當年足以在台北市買房了。

後來台灣又陸續捕撈過多次,最高售價曾達新台幣一百萬元,所以又被暱稱為「百萬富翁螺」。最近一次是在2020年澎湖現身。

龍宮翁戎螺和其他貝類的螺旋線隱藏著大自然的數學奧秘,與龍宮翁戎螺同一時代的菊石和鸚鵡螺,生長的螺線比例近似於「費氏數列」。

所謂費氏數列是一個整數數列,每一個數值等於前兩個數相加的和,也就是:
0, 1, 1, 2, 3, 5, 8, 13, ....

f(0) = 0
f(1) = 1
f(n) = f(n-1) + f(n-2), n > 1


https://www.geeksforgeeks.org/wp-content/uploads/fibonacci-sequence.png

西方第一個研究這個數列的是12世紀義大利人費波那契(Fibonacci),故名費氏數列,他在研究兔子繁殖時,發現這個數列。因為兔子繁殖週期只有約45天,生下來半年就能繁殖,所以一對兔子一年下來可以生很多兔子。

後來科學家發現,不只是兔子,還有很多動植物(如貝類、蜜蜂、蕨類、向日葵、鳳梨⋯等等),甚是颱風或銀河的形狀,都可以看到費氏數列的痕跡,所以這個規則簡單的數列,竟然與天地萬物生長有關係。

而且費氏數列的每個數值與前一數值的比例,會趨近於1.618,這是古希臘人發現的「黃金比例」,是人類視覺感官覺得最協調的比例,經常應用在建築與工業設計上。

對費氏數列有興趣的同學,可以參考前太空中心主任吳作樂的文章。https://www.taiwannews.com.tw/ch/news/3472903

" If you count the number of spirals on any of these sculptures you will find that they are always Fibonacci numbers."

同樣從大自然觀察中發現奧秘,道德經42章說:「道生一,一生二,二生三,三生萬物」,以道家觀點來說,道即混沌,相當於0;混沌中產生太極,於是0產生1;有了混沌(0)跟太極(1),就能分出陰陽(2),然後太極與陰陽演化出萬物,也就是「一生二,二生三,三生萬物」,見解跟費氏數列不謀而合。

下面範例1-5c利用第4課所學的陣列以及本課 if條件句來計算費氏數列。

// 1-5c 費氏數列(Fibonacci Sequence)
// Created by Heman, 2021/07/15
var 數列 = [0, 1]
var 最大索引 = 1

func 費氏數列(_ n: Int) -> Int {
if n < 0 { return -1 }
if n <= 最大索引 {
return 數列[n]
}
let m = 最大索引 + 1
for i in m...n {
let 下一個 = 數列[i-1] + 數列[i-2]
數列 = 數列 + [下一個]
}
最大索引 = n
return 數列[n]
}

for i in 0...92 {
print(i, 費氏數列(i))
}
// print(費氏數列(93))

在我們前面第4課的範例程式中,陣列都是定義為常數,因為商王的歷史資料是固定的。但實際上,陣列的變數更常用到,也就是說,陣列元素是可增可減的,陣列變數的內容不管是元素個數或元素值都可變。

根據費氏數列的定義
f(0) = 0
f(1) = 1
f(n) = f(n-1) + f(n-2), n > 1

我們用一個整數陣列的變數「數列」來儲存費氏數列的計算結果,一開始「數列」只儲存兩個初始值:f(0)=0, f(1)=1,然後根據 f(n) = f(n-1) + f(n-2), n > 1 來計算後面的元素,每計算一個新的元素,就加入到「數列」的末尾。
    let 下一個 = 數列[i-1] + 數列[i-2]
數列 = 數列 + [下一個]

就是將計算出來的「下一個」費波那契數加方括號 [ ],[下一個],變成一個小陣列(只有一個元素),再讓兩個陣列用加號 + 相加,結果是兩個陣列合併,後者元素併入到前者的末尾。

函式「費氏數列(n)」會傳回第n個(索引為 n)費波那契數,如果 n <= 最大索引,就表示這個值之前已經算過,直接從「數列」中取得,如果 n > 最大索引,表示沒有算過,就必須從(最大索引+1)開始算,直到第n個為止。

實際執行結果發現,Swift 64位元整數最大也只能算到第92個費波那契數(19位數),若要計算更大的費氏數列,該如何辦呢?Swift 當然有方法可以做更大數目的科學運算,但已超出本課程單元1的範圍,以後有機會再來做「數值分析」的單元。


若再仔細觀察費氏數列,會發現兩奇一偶的規律,也就是兩個奇數、一個偶數的節奏重複出現。其實費氏數列還有很多有趣的問題,例如說,費氏數列裡面有質數嗎?費氏數列中有哪些是整數的平方?費氏數列跟巴斯卡三角形有何關係?費氏數列竟可用來預測股市?⋯⋯等等,都等待你去探索。

註解
  1. 費波那契(Leonardo Fibonacci, 1175-1250)是義大利數學家,除了費波那契數之外,他也引進印度人發明的阿拉伯數字 0, 1, 2, 3...取代當時的羅馬數字 I, II, III, IV, ...,由於羅馬數字沒有 0 與進位的概念,所以如果沒有阿拉伯數字,萊布尼茲也無法發明二進位,我們現在也就沒有電腦可用。
  2. 費波那契數的「密度」比質數低很多,是費波那契數同時也是質數的更是稀少,目前已驗證的只有34個,第34個「費波那契質數」乃是第104,911個費波那契數,有2萬位數。https://en.wikipedia.org/wiki/Fibonacci_prime
#20. 進度回顧與習題

我想在此先暫停,確認一下大家是否都跟上進度。

前面這5課是 Swift 基本語法,由淺而深,希望大家都能充分理解,想要自己寫程式,必須熟悉所有的「關鍵字」用法,包括:
let   定義一個常數
var 定義一個變數
func 定義一個函式
for-in for 迴圈
repeat-while repeat 迴圈
if-else if 條件句
print() 列印到主控台的函式
abs() 傳回絕對值的函式
Int 整數類型
Float 浮點數類型
Double 實數類型
String 字串類型
Bool 布爾(邏輯)類型

還有千萬不要忽略標點符號的重要性,Swift 程式裡面的每一個標點符號都有特殊的用法與含義,重要性不下於關鍵字,目前所教過的標點符號包括:
:        冒號表示屬於某資料類型
, 逗號分隔同類的宣告、陣列中的元素
... 三個連續句號用來界定一個資料範圍
" " 雙引號標明「字串值」
\ 倒斜線用在雙引號裡面,表示有特殊意義的字串值
// 雙斜線用來寫程式註解
-> 箭頭用在函式傳回的資料類型
( ) 小括號用途比較多,可用在運算式的分組(改變優先順序)、函式的參數等等
[ ] 中括號用來構成「陣列」
{ } 大括號用來包含若干指令,形成「指令段落」
= 給變數或常數「指定」資料值
+ - * / 數值的加減乘除
% 百分比符號用來取餘數(a % b 得到 a 除以 b 的餘數)
==, !=, <, >, <=, >= 邏輯運算符號
? : 條件取值的便捷語法(例如 x = n < 0 ? -1 : 1)
_ 底線用在函式的參數名稱之前,表示呼叫函式時,該參數名稱可省略

記得這些關鍵字與標點符號,必須用英文模式打,千萬不能用中文模式,而且英文關鍵字是區分大小寫的。

這樣列出來一看,其實也不少了,每一個都要熟悉需要花點時間,因此,如果中間有講解不夠清楚的地方,請大家提出來,公開回覆或私訊給我都可以,但是不要用個別內容的「留言」,因為留言不會有通知,所以不容易注意到。

如果都覺得沒有問題,請試試以下習題,完成後可將程式碼與執行畫面擷取下來私訊給我。

習題
(1) 寫個因數分解的函式,參數為任意整數n,傳回n的所有因數。
(2) 寫個從1001到2001的「質因數」分解的程式
(3) 列出巴斯卡三角形(到15階)-- 這一題比較難,範例輸出如下
文章分享
評分
評分
複製連結
請輸入您要前往的頁數(1 ~ 9)

今日熱門文章 網友點擊推薦!