流程控制

Only miss the sun when it starts to snow.

Passenger

認識向量中我們提到有關於邏輯值向量在判斷條件或者資料篩選的應用,其中資料篩選應用已經在操作向量中示範透過邏輯篩選(logical filtering)的技法,能夠比使用索引(indexing)、切割(slicing)更有效從向量中選擇出符合判斷條件為 TRUE 的元素,而這個效率優勢在面對容納大筆資料的長向量時會更為明顯。而判斷條件應用則是可以幫助我們在程式中撰寫不同的劇本,再讓 R 語言執行時根據判斷條件走不同的程式分支(branch)。除了不同程式劇本的需求,也常會有重複執行、需要大量手動複製貼上的需求,這時就會需要應用迴圈(loop)或叫做迭代(iteration)的技法;我們將程式分支與迴圈迭代這兩個技法總稱為流程控制。

從流程控制開始我們需要撰寫多行且具有縮排的程式,這時候使用左上角的來源(Source)區塊編寫就比在命令列(Console)撰寫來得適合。首先新增一個 R 程式:

新增一個 R 程式

寫作多行程式並將這些程式選擇起來點選執行:

寫作多行程式並點選執行

多行的程式就會在命令列被執行:

多行的程式會在命令列被執行

利用 if 搭建一個程式分支

我們使用保留字 if 搭配長度為 1 的邏輯值向量就能夠搭建出一個程式分支,它的外觀架構長得像這樣子。

if (長度為 1 的邏輯值向量) {
  # 程式
}
1
2
3

假如小括號中長度為 1 的邏輯值向量為 TRUE ,R 語言就會執行大括號縮排內的程式,反之若小括號中長度為 1 的邏輯值向量為 FALSE ,R 語言就會略過大括號縮排內的程式不執行;舉例來說,一個喜愛運動的人早上起床會看天氣決定當天的行程,如果是晴天就出門跑步;程式使用 sample() 函數從一個描述天氣的文字向量中選出一個,由於只搭建了一個程式分支,只有在選出的文字向量為晴天的情況下才會印出"天氣是晴天,出門跑步"

weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
weather <- sample(weathers, size = 1)
if (weather == "晴天") {
  sprintf("天氣是%s,出門跑步", weather)
}
1
2
3
4
5
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather == "晴天") {
## +   sprintf("天氣是%s,出門跑步", weather)
## + }
## [1] "天氣是晴天,出門跑步"
1
2
3
4
5
6

利用 if 與 else 搭建兩個程式分支

我們使用保留字 ifelse 搭配長度為 1 的邏輯值向量就能夠搭建出兩個程式分支,它的外觀架構長得像這樣子。

if (長度為 1 的邏輯值向量) {
  # 程式
} else {
  # 程式
}
1
2
3
4
5

假如小括號中長度為 1 的邏輯值向量為 TRUE ,R 語言就會執行第一個大括號縮排內的程式然後離開這個判斷架構,若小括號中長度為 1 的邏輯值向量為 FALSE ,R 語言會改為執行第二個大括號縮排內的程式;舉例來說,一個喜愛運動的人早上起床會看天氣決定當天的行程,如果是晴天就出門跑步、如果不是晴天就在家當一個沙發馬鈴薯;程式使用 sample() 函數從一個描述天氣的文字向量中選出一個,這次搭建了兩個程式分支,在選出的文字向量為晴天的情況下印出 "天氣是晴天,出門跑步"

weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
weather <- sample(weathers, size = 1)
if (weather == "晴天") {
  sprintf("天氣是%s,出門跑步", weather)
} else {
  sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
}
1
2
3
4
5
6
7
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather == "晴天") {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是晴天,出門跑步"
1
2
3
4
5
6
7
8

如果選出的文字向量不是晴天,就會印出"天氣是XX,當一個沙發馬鈴薯"

## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather == "晴天") {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是多雲,當一個沙發馬鈴薯"
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather == "晴天") {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是小雨,當一個沙發馬鈴薯"
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather == "晴天") {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是大雨,當一個沙發馬鈴薯"
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather == "晴天") {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是暴風雨,當一個沙發馬鈴薯"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

利用 if、else if 與 else 搭建三個以上的程式分支

我們使用保留字 ifelse ifelse 搭配長度為 1 的邏輯值向量就能夠搭建出三個以上的程式分支,它的外觀架構長得像這樣子。

if (長度為 1 的邏輯值向量) {
  # 程式
} else if (長度為 1 的邏輯值向量) {
  # 程式
} else if (長度為 1 的邏輯值向量) {
  # 程式
} else {
  # 程式
}
1
2
3
4
5
6
7
8
9

假如第一個小括號中長度為 1 的邏輯值向量為 TRUE ,R 語言就會執行第一個大括號縮排內的程式然後離開這個判斷架構;若第一個小括號中長度為 1 的邏輯值向量為 FALSE ,R 語言會改為觀察第二個小括號中長度為 1 的邏輯值向量是否為 TRUE ,假如是就會執行第二個大括號縮排內的程式然後離開這個判斷架構;若第二個小括號中長度為 1 的邏輯值向量仍舊為 FALSE ,R 語言會改為觀察第三個小括號中長度為 1 的邏輯值向量是否為 TRUE ,假如是就會執行第三個大括號縮排內的程式然後離開這個判斷架構;若第三個小括號中長度為 1 的邏輯值向量仍舊為 FALSE ,R 語言就會逕自執行 else 後第四個大括號縮排內的程式然後離開這個判斷架構。舉例來說,一個喜愛運動的人早上起床會看天氣決定當天的行程,如果是晴天或者多雲就出門跑步、如果是小雨則去健身房,若是大雨或暴風雨就在家當一個沙發馬鈴薯;程式使用 sample() 函數從一個描述天氣的文字向量中選出一個,這次搭建了三個程式分支,在選出的文字向量為晴天或多雲的情況下印出 "天氣是XX,出門跑步" ,這裡可以使用 %in% 這個判斷符號聯集兩個條件:(weather == "晴天") | (weather == "多雲")

weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
weather <- sample(weathers, size = 1)
if (weather %in% c("晴天", "多雲")) {
  sprintf("天氣是%s,出門跑步", weather)
} else if (weather == "小雨") {
  sprintf("天氣是%s,去健身房", weather)
} else {
  sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
}
1
2
3
4
5
6
7
8
9
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather %in% c("晴天", "多雲")) {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else if (weather == "小雨") {
## +   sprintf("天氣是%s,去健身房", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是晴天,出門跑步"
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather %in% c("晴天", "多雲")) {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else if (weather == "小雨") {
## +   sprintf("天氣是%s,去健身房", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是多雲,出門跑步"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

如果選出的文字向量是小雨,就會印出"天氣是小雨,去健身房"

## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather %in% c("晴天", "多雲")) {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else if (weather == "小雨") {
## +   sprintf("天氣是%s,去健身房", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是小雨,去健身房"
1
2
3
4
5
6
7
8
9
10

如果選出的文字向量既不是晴天、多雲亦不是小雨,那就是大雨或暴風雨,會運用 else 判斷印出"天氣是XX,當一個沙發馬鈴薯"

## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather %in% c("晴天", "多雲")) {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else if (weather == "小雨") {
## +   sprintf("天氣是%s,去健身房", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是大雨,當一個沙發馬鈴薯"
## > weathers <- c("晴天", "多雲", "小雨", "大雨", "暴風雨")
## > weather <- sample(weathers, size = 1)
## > if (weather %in% c("晴天", "多雲")) {
## +   sprintf("天氣是%s,出門跑步", weather)
## + } else if (weather == "小雨") {
## +   sprintf("天氣是%s,去健身房", weather)
## + } else {
## +   sprintf("天氣是%s,當一個沙發馬鈴薯", weather)
## + }
## [1] "天氣是暴風雨,當一個沙發馬鈴薯"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

運用 for 迴圈解決重複的任務

在 R 語言中有一個內建文字向量 month.name,這個文字向量的長度為 12,裡頭記錄了 12 個月份的英文名稱,假如現在有一個不明所以的任務,需要將這 12 個英文月份名稱一一輸出在命令列。土法煉鋼的方法是我們可以像這樣複製貼上程式碼,然後修改中括號 [] 裡面的索引值,由 1 改到 12。

month.name # # 1-12 月的月份名稱
month.name[1]
month.name[2]
month.name[3]
month.name[4]
month.name[5]
month.name[6]
month.name[7]
month.name[8]
month.name[9]
month.name[10]
month.name[11]
month.name[12]
1
2
3
4
5
6
7
8
9
10
11
12
13
## > month.name # # 1-12 月的月份名稱
##  [1] "January"   "February"  "March"     "April"     "May"       "June"      "July"     
##  [8] "August"    "September" "October"   "November"  "December" 
## > month.name[1]
## [1] "January"
## > month.name[2]
## [1] "February"
## > month.name[3]
## [1] "March"
## > month.name[4]
## [1] "April"
## > month.name[5]
## [1] "May"
## > month.name[6]
## [1] "June"
## > month.name[7]
## [1] "July"
## > month.name[8]
## [1] "August"
## > month.name[9]
## [1] "September"
## > month.name[10]
## [1] "October"
## > month.name[11]
## [1] "November"
## > month.name[12]
## [1] "December"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

碰到這樣需要大量手工複製貼上的事情,可以求助迴圈來幫助我們。首先登場的 for 迴圈外觀架構長得像這樣。

for (ITERATOR in ITERABLE) {
  # 每次迭代要執行的程式
}
1
2
3

在第一次的迭代中,ITERATOR 是 ITERABLE[1]、在第二次的迭代中,ITERATOR 是 ITERABLE[2];以此類推至第 N 次的迭代,ITERATOR 是 ITERABLE[N],其中 ITERATOR 是長度為 1 的向量,ITERABLE 是長度為 N 的向量。 在我們目前的例子中,ITERATOR 必須由 1、2 經過 12 次的迭代更動至 12,因此可以設計一個 ITERATOR 名稱為 i,ITERATBLE 是一個含有 1 到 12 的數值向量,利用 1:12 產生,在每一次迭代時都會執行大括號裡面的程式 print(month.name[i])

for (i in 1:12) {
  print(month.name[i])
}
1
2
3
## > for (i in 1:12) {
## +   print(month.name[i])
## + }
## [1] "January"
## [1] "February"
## [1] "March"
## [1] "April"
## [1] "May"
## [1] "June"
## [1] "July"
## [1] "August"
## [1] "September"
## [1] "October"
## [1] "November"
## [1] "December"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

也能夠設計一個 ITERATOR 名稱為 m,ITERATBLE 是一個含有 1 到 12 月份英文名稱的文字向量 month.name ,在每一次迭代時都會執行大括號裡面的程式 print(m)

for (m in month.name) {
  print(m)
}
1
2
3
## > for (m in month.name) {
## +   print(m)
## + }
## [1] "January"
## [1] "February"
## [1] "March"
## [1] "April"
## [1] "May"
## [1] "June"
## [1] "July"
## [1] "August"
## [1] "September"
## [1] "October"
## [1] "November"
## [1] "December"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

運用 while 迴圈解決重複的任務

接著登場的 while 迴圈外觀架構長得像這樣。

while (長度為 1 的邏輯值向量) {
  # 每次迭代要執行的程式
}
1
2
3

在每一次的迭代之前,R 語言都會去檢查小括號中長度為 1 的邏輯值向量是否為 TRUE,是的話就會執行每次迭代要執行的程式;一但這個邏輯值向量是 FALSE 就會離開迴圈。因此如果我們想將內建向量 month.name 一一輸出,可以 while 迴圈寫好。

i <- 1
while (i < 13) {
  print(month.name[i])
  i <- i + 1
}
1
2
3
4
5
## > i <- 1
## > while (i < 13) {
## +   print(month.name[i])
## +   i <- i + 1
## + }
## [1] "January"
## [1] "February"
## [1] "March"
## [1] "April"
## [1] "May"
## [1] "June"
## [1] "July"
## [1] "August"
## [1] "September"
## [1] "October"
## [1] "November"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

很重要的一行程式為 i <- i + 1,假如忘記寫這一行程式,我們的 while 迴圈會無限次數地一直輸出 [1] "January",原因是小括號中長度為 1 的邏輯值向量永遠判斷為 TRUE(1 < 13),所以不斷地執行 print(month.name[i])

兩種迴圈的運用時機

那麼在實際撰寫 R 語言程式的時候,我們何時應該運用 for 迴圈、何時應該運用 while 迴圈呢?一個簡單的判斷是:假如我們明確知道程式需要執行幾次(迭代次數),比如說我們知道 month.name 這個文字向量的長度為 12,就可以採用 for 迴圈或 while 迴圈,端看個人偏好;而在不知道迭代次數的情形下,我們就只能被迫採用 while 迴圈。

這個說法還是略嫌抽象,舉一個例子說明會比較好理:假如現在投擲一個公正的骰子(裡頭沒有灌鉛或水銀),想知道總共需要投擲幾次才能夠將 1 到 6 點都至少投出一次,這就是一個不知道迭代次數的問題。運氣超好也許投擲六次每個點數就各自恰巧出現一次;運氣差一點的也許要投擲一、二十次才湊得齊所有點數,我們寫一段程式來模擬這個過程,程式中使用 unique() 函數來偵測過往的投擲結果是否有六個不同的數值(點數。)

dice <- 1:6 # 1 代表 1 點、2 代表 2 點,以此類推
rolling_history <- c() # 建立一個空的向量來放置每一次投擲的結果
while (length(unique(rolling_history)) < 6) {
  dice_roll <- sample(dice, size = 1) # 投擲!
  rolling_history <- c(rolling_history, dice_roll) # 將每次投擲記錄起來
}
# 將結果印出
sprintf("總共投擲了 %s 次", length(rolling_history))
rolling_history
1
2
3
4
5
6
7
8
9
## > dice <- 1:6 # 1 代表 1 點、2 代表 2 點,以此類推
## > rolling_history <- c() # 建立一個空的向量來放置每一次投擲的結果
## > while (length(unique(rolling_history)) < 6) {
## +   dice_roll <- sample(dice, size = 1) # 投擲!
## +   rolling_history <- c(rolling_history, dice_roll) # 將每次投擲記錄起來
## + }
## > # 將結果印出
## > sprintf("總共投擲了 %s 次", length(rolling_history))
## [1] "總共投擲了 11 次"
## > rolling_history
##  [1] 2 1 5 5 2 2 4 4 2 6 3
1
2
3
4
5
6
7
8
9
10
11

以這次執行結果來看,共投了 11 次才將 6 個點數至少投出一次;歷次投擲分別為 2 點、1 點、5 點、5 點、2 點、2 點、4 點、4 點、2 點、6 點與 3 點,您可以試著自行執行幾輪,看看每一輪投擲次數的差異。

結合程式分支與迴圈迭代

在撰寫時將流程控制的兩個技巧:程式分支與迴圈迭代合併使用會讓我們的需求更有彈性, 亦可以運用保留字 break 或者 next 來協助。保留字 break 能夠讓我們在迴圈迭代的過程中在長度為 1 的邏輯值向量為 TRUE 的時候(亦即滿足某個判斷條件)離開迴圈,舉例來說將內建文字向量 month.name 一一印出時,假如碰到 "August" 就離開迴圈。

for (m in month.name) {
  if (m == "August") {
    break
  } else {
    print(m)
  }
}
1
2
3
4
5
6
7
## > for (m in month.name) {
## +   if (m == "August") {
## +     break
## +   } else {
## +     print(m)
## +   }
## + }
## [1] "January"
## [1] "February"
## [1] "March"
## [1] "April"
## [1] "May"
## [1] "June"
## [1] "July"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

保留字 next 能夠讓我們在迴圈迭代的過程中在長度為 1 的邏輯值向量為 TRUE 的時候(亦即滿足某個判斷條件)略過該次迭代但是繼續完成迴圈,舉例來說將內建文字向量 month.name 一一印出時,假如碰到 "August" 就略過該次迭代。

for (m in month.name) {
  if (m == "August") {
    next
  } else {
    print(m)
  }
}
1
2
3
4
5
6
7
## > for (m in month.name) {
## +   if (m == "August") {
## +     next
## +   } else {
## +     print(m)
## +   }
## + }
## [1] "January"
## [1] "February"
## [1] "March"
## [1] "April"
## [1] "May"
## [1] "June"
## [1] "July"
## [1] "September"
## [1] "October"
## [1] "November"
## [1] "December"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

小結

在這個小節中我們簡介 R 語言為因應不同程式劇本與重複執行等需求所發展的流程控制技法:程式分支與迴圈迭代,如何利用 if 搭建一個程式分支、如何利用 ifelse 搭建兩個程式分支、如何利用 ifelse ifelse 搭建三個以上的程式分支、運用 for 迴圈或 while 迴圈解決重複的任務、兩種迴圈的運用時機以及結合程式分支與迴圈迭代。

練習

  • 我們建立了一個 week 向量,裡面有一個星期中的七天名稱,請您使用 for 迴圈一一輸出每一天
week <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
# 使用 for 迴圈一一輸出每一天
1
2
  • 同樣的一個 week 向量,請您使用 while 迴圈一一輸出每一天
week <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
# 使用 while 迴圈一一輸出每一天
1
2
  • 同樣的一個 week 向量,請您在使用迴圈一一輸出每一天的時候略過週一到週五,只輸出我們最愛的週末兩天即可
week <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
# 略過週一到週五,只輸出我們最愛的週末兩天
1
2
  • (Fizz Buzz 問題) 請您使用迴圈一一輸出 1 到 100 這 100 個數字,其中在碰到 3 的倍數時候改為輸出 "fizz",在碰到 5 的倍數時候改為輸出 "buzz",在碰到 15 的倍數時候改為輸出 "fizz buzz"

延伸閱讀