python for迴圈和斐波那契

王慶帥發表於2020-10-20

轉發忘了作者
for迴圈
以c語言為例,for迴圈幾乎是同while迴圈完全相同的功能。在Python中,for迴圈經過全新的設計,實際只支援一個功能,當然也是程式設計最常用到的功能,就是“遍歷”。
所謂遍歷(Traversal),是指沿著某條確定的搜尋路線,依次對序列中的每個結點(每個元素)均做一次且僅做一次訪問。
比如最常見的字串,實際就是一個序列。字串“abcdefg”,中間包含7個字母,每個字母就是一個結點。“沿著某條確定的搜尋路線”,其實指的就是按照何種規則來順序訪問字串中的每個結點。最常見的可以使從開始到結尾,或者從結尾到開始。
我們先使用while迴圈來做一個字串的遍歷:

s=“abcdefg”
i = 0
while i < len(s):
print(s[i])
i += 1
在這個例子中,我們先定義了一個字串,設定迴圈初始值0。while迴圈的邊界條件使用了內建標準函式len(),這個函式的功能是給出引數中包含的元素個數,在這裡是字元的個數。
隨後在迴圈體中我們使用print函式在每次迴圈中列印出來一個結點(一個字元)。s[i]是s字串中,第i個字元(結點)的意思,i在這裡有一個專有名詞叫做“下標”,你可以相像數學中常用的Si形式。這種訪問某個序列中具體某個元素的方式是今天的重點之一。
這裡i的取值範圍是從0開始,因此最大可以到字串中字元總數-1。最後的i += 1,指的是按照從串頭到串尾的方式,迴圈訪問整個字串中的所有字元。程式的執行結果是這個樣子:

a
b
c
d
e
f
g
補充一個小知識,剛才的迴圈中,我們使用了while i < len(s):,這可以工作的很好,理解起來也不難。但實際上,下面這樣做效率更高:
n=len(s)
while i < n:

原因是,在前一個寫法中,len這個函式會執行很多次,迴圈每一次都要重新執行。而在後面的寫法中,len函式只需要執行一次。在其後的迴圈中,直接使用一個變數的值就要快多了。

遍歷是程式設計中最常用到的操作,也是最簡單的演算法,希望你理解“遍歷”的含義了。
接下來我們看一看for迴圈來實現上面同樣的功能:

for a in “abcdefg”:
print(a)
僅有兩行程式碼,完成跟上面while迴圈程式完全相同的功能,簡潔了很多。執行結果跟上面完全一樣,就不再重貼了。為了便於理解,我使用虛擬碼把for迴圈的基本形式重寫一遍:

for 遍歷變數 in 序列型的資料:
迴圈體,每次迴圈執行一遍,每次“遍歷變數”會有一個新值
這就是for迴圈的最基本形式。for/in/:是Python中的保留字。迴圈最終會執行的次數,等同於“序列型資料”中的元素個數。“遍歷”是對所有元素都要迴圈訪問一遍。

列表
for迴圈遍歷的物件必須是一個序列型別。序列型別並不是在Python中有一種特定的型別,而是一種統稱。可以理解為有順序、能順序訪問的型別都叫序列型別。列表型別是序列型別的一種。字串型別也是序列型別的一種。
先看看數字的列表。這是一個數字列表的樣子:

[2,3,8.3,34,55,23]
使用中括號圈起來的,一組用逗號隔開的元素,就是列表。列表是Python六大資料型別中的一種,我們現在已經學習過了3種基本資料型別,數字、字串、列表。這一講我們只是簡單引入列表的概念,來幫助我們理解“遍歷”,在第八講中,我們將正式而且更深入的講解列表這種資料型別。
跟字串一樣,對數字的列表同樣可以使用len函式:

len([2,3,8.3,34,55,23])
6
我們同樣可以使用for迴圈對數字列表進行遍歷,比如:

datas = [2,3,8.3,34,55,23];
for x in datas:
print(x)
#下面是執行的結果:
2
3
8.3
34
55
23
上面的數字列表中,我們混合了整數和浮點小數。從技術上講,列表中還可以同時包含“布林”和“字串”型別的資料。只是因為不同的資料型別,難以有共同的處理方式,放到同一個列表中也沒有辦法得到程式效率上的優勢,所以並不推薦那樣使用。
只要是列表的形式,就可以使用for迴圈來進行遍歷操作,從而提高處理速度。
我們再來對比遍歷數字列表的while迴圈模式和for迴圈模式:

#首先看while迴圈
i=0
while i<5:
print(i)
i += 1

#下面是for迴圈的方式
for i in [0,1,2,3,4]:
print(i)

#兩種迴圈的執行結果都是一樣的:
0
1
2
3
4
可以看到,for迴圈專門為了遍歷操作而生,在處理序列資料的時候,程式簡潔、程式碼少、效率高。而while迴圈則有更強的通用性,但在處理遍歷任務的方面則略微麻煩。
為了讓for能夠處理更多通用的任務,Python提供了一個內建的標準函式range來自動生成一個序列,使用方法的虛擬碼是:

#單引數方式,生成由0開始,到小於最大值的整數序列
range(最大值)

#雙引數方式,生成由最小值到最大值(不包含最大值本身)的整數序列
range(最小值,最大值)

#三引數模式,生成由最小值到最大值,以步長為遞增的序列
range(最小值,最大值,步長)
我們來看一組實際使用的例子來加深印象:

for i in range(5):
print(i)
#執行結果是:
0
1
2
3
4

for i in range(1,6):
print(i)
#執行結果是:
1
2
3
4
5

for i in range(1,10,2):
print(i)
1
3
5
7
9
range的注意事項是:range的引數、返回的序列都必須是整數。
我們前面見過了很多操作符,長得很不像關鍵字,比如+、-,今天我們終於看到了一個相反的例子,in操作符更像關鍵字,而不像操作符。當然操作符屬於關鍵字的一種。
除了在for迴圈中使用in操作符,in還可以用於邏輯判斷。比如:

“北京” in “今天下雨的地區有:北京、天津、河北” #結果是true
59 in range(60,101) #結果為:false
挑戰
我們今天的挑戰內容是程式設計生成斐波那契數列(Fibonacci sequence)。
斐波那契數列指的是這樣一個數列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…,這個數列從第3項開始,每一項都等於前兩項之和。
今天學習的主要內容是for迴圈,所以當然這個挑戰要使用for迴圈來完成,生成斐波那契數列的前100項。
老辦法,請大家先認真思考,用流程圖或者虛擬碼描述自己的思路,覺得思路清晰了,再看下面的內容。

我們繼續使用快速原型法,首先是理清程式的需求,當做註釋內容寫入到程式:

“”"
使用for迴圈生成前100項斐波那契數列

斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…
這個數列從第3項開始,每一項都等於前兩項之和。
“”"
接下來我們梳理在程式主體迴圈之前應當準備好的變數和初始值:

#以序列中任意連續3個數字來看
#a代表其中第一個數字,初始是1
a = 1
#b代表其中第二個數字,初始是1
b = 1
#c代表第三個數字,應當是a+b的和,但當前尚未進入迴圈,所以賦值為0
#因為python語言使用變數前無需宣告,所以實際上c=0可以省略
c = 0
#遍歷所用變數在for迴圈中定義,這裡忽略
跟上一講的例子不同,斐波那契數列肯定是邊生成邊輸出,所以肯定是要在迴圈之內來完成輸出的工作。所以不像上一講的例子,可以先確定輸出的內容。
直接進入到考慮迴圈體的環節,首先依然是迴圈的邊界:

#從第3項開始,迴圈到第101項
for i in range(3,101):
迴圈到101項的意思是因為,前面講過了,range函式所產生的序列,不包含給定的最大值本身,所以range(3,101)實際會產生從3、4、5到100的序列。
參考前面的內容,我們把主體部分的內容一起列出來:

#前兩項不用計算,直接顯示
print(“第 1 項為:”,a)
print(“第 2 項為:”,b)

#從第3項開始,迴圈到第101項
for i in range(3,101):
c = a + b #計算第三項
a = b #3個元素的視窗向後移,原第一個元素被拋棄
b = c #第二個元素更新為新計算的項
print(“第”,i,“項為:”,c) #顯示
為了看起來更清楚,我們這次使用截圖來展示上面程式的輸出結果:
seqs1

程式優化
有人說“好文章是改出來的。”,其實好的程式也是一樣。程式編寫第一項任務是完成需求所定義的基本工作。隨後就要根據程式的表現,有針對性的優化。程式優化最基本的任務通常是速度和記憶體的佔用。因為我們目前的學習還比較基礎,暫時不會涉及到那些部分,所以我們先對程式的結構和程式碼量進行優化。目標是結構更清晰易讀,程式碼更精簡高效。
首先我們要根據當前程式的情況進行分析評估,根據評估的結果決定下一步的改進方向。以當前的程式情況來說,可以容易的發現以下幾項問題:

斐波那契數列生成的過程中,前兩項的生成是單獨處理的,跟後面的98項不統一,這會造成將來對程式修改、重用的時候,這兩項都要單獨處理,維護性差。
也因為對頭兩項單獨的處理,多次使用了print函式,造成程式碼冗餘。
變數c在顯示完成後實際可以不用儲存,沒有必要使用,這造成記憶體的浪費。
最後是沒有進行函式化,可重用性差。
根據我們的分析結果,進行程式優化之前,我們補充一點知識。在第四講的做練習的時候,為了求甲、乙雙方的速度,我們曾經自定義一個函式,最後求得結果的時候是這樣一句:

#計算原題:當甲乙雙方相距36千米時雙方的速度
x,y = getSpeed(36) #getSpeed函式,最後使用了return x,y
這種使用方法很自然,跟單獨一個變數的賦值比起來,效率也更高。我們在這裡總結一下為變數賦值的幾種形式:

#常規的賦值
a=1
c=“abcd”

#多元賦值
x,y = 2,3
x,y = 3,2
x,y = y,x #注意不是數學等式,這是交換兩個變數的值
x,y = y,x+b

#連續賦值
a=b=c=d=10 #賦值結束後,變數a/b/c/d都將是10
好了,我們對程式進行優化。剛才講到的多元賦值也能用來優化這個程式:

“”"
使用for迴圈生成前100項斐波那契數列
作者:Andrew

斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…
這個數列從第3項開始,每一項都等於前兩項之和。
“”"
a = 1
b = 1

print(“第 1 項為:”,a)

for i in range(2,101):
print(“第”,i,“項為:”,b) #顯示
a,b = b,a+b #採用多元賦值,直接完成下一項計算和視窗的後移

不錯吧?怎麼看都能感覺到清晰的進步。然而,兩個存在的問題依然沒有解決:

佇列中第一項數字仍然單獨處理;
仍然沒有函式化。
函式化其實比較簡單,把第一項數字也納入整體生成的考慮就需要演算法的調整。這個過程一般只能進行數學上的分析和經驗的積累。所以這裡我直接說答案:
在第一版的時候,我們使用了3個數字的“視窗”,因為第三個數字是前兩個數字之和。
第二版的優化,我們知道了第三個數字其實可以省略不儲存,所以只使用了2個數字的“視窗”,因為佇列中第三個數字需要前兩個數字之和,所以這2個數字的視窗,實際無法繼續省略。
既然無法省略,並且要保持2個數字的視窗。我們把數字向前延伸一位,增加一個第0項,值是0,並且無需顯示,這個問題就簡單了,直接看原始碼:

#我們省略了開始的註釋
def fibonacci(n):
#為斐波那契數列之前新增一個不顯示的第0項:0
#0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…
#以序列中任意連續2個數字來看
#a代表其中第一個數字,初始是0
#b代表其中第二個數字,初始是1
a,b = 0,1 #使用連續賦值簡化程式碼

#從第1項開始,迴圈到第n項,結尾邊界為n+1
for i in range(1,n+1):
    print("第",i,"項為:",b)    #顯示
    a,b = b,a+b #採用多元賦值,直接完成下一項計算和視窗的後移

#呼叫函式,生成前100項
fibonacci(100)

相關文章