這一篇教程的學習目標是瞭解什麼是遞歸(Recursion)。
簡單來說,遞歸就是函數自己調用自己。(聽起來…好淫蕩…)
但是,自己調用自己不會變成無限循環調用麼?
例如下面這個代碼:
def recursion(): return recursion() recursion()
代碼運行後會拋出異常,RecursionError: maximum recursion depth exceeded
意思是,遞歸錯誤:超過最大遞歸深度
也就是說,因爲函數不停的循環調用自身超過了一定次數導致的異常。
這種叫無窮遞歸(Infinite Recursion),一般來說並沒有什麼用。
我們需要有用的遞歸。
比較經典的應用有階乘運算、冪運算和二分查詢(看到這些詞語別暈,實際上很簡單)。
我們先來看階乘運算。
階乘:一個正整數的階乘是所有小於及等於該數的正整數的積
舉例來說,5的階乘就是5*4*3*2*1,4的階乘就是4*3*2*1。
其實,階乘的運算如果不使用遞歸我們也可以實現。
示例代碼:
def factorial(n): result = n for i in range(1, n): result *= i return result print(factorial(5)) # 顯示輸出結果爲:120
然後,我們來看使用遞歸方式的實現。
示例代碼:
def factorial(n): if n == 1: return 1 else: return n * factorial(n - 1) # 函數中調用函數自身 print(factorial(5)) # 顯示輸出結果爲:120
遞歸方式實現的階乘,好像有些不太容易理解。
我們把計算過程中,參數變量的值通過print語句顯示出來看一下。
修改後的代碼:
def factorial(n): print(n) # 顯示輸出參數值 if n == 1: return 1 else: return n * factorial(n - 1) factorial(5)
當我們運行代碼,得到如下結果:
這意味着print語句被執行了5次,同時也意味着函數被調用了5次。
先記下這個特點,我們繼續通過print語句把每次通過return語句計算的結果也顯示出來。
修改後代碼如下:
def factorial(n): if n == 1: print('1') # 顯示輸出當前的階乘結果 return 1 else: current = n * factorial(n - 1) print(current) # 顯示輸出當前的階乘結果 return current factorial(5)
當我們運行代碼,得到如下結果:
這個結果意味着,每次調用函數都會進行一次計算,並且將計算結果通過return語句返回。
但是,遞歸的具體執行過程仍然很模糊。
我們來看一個示意圖。
上方5個色塊,是代碼調用了5此函數。
每次調用函數都會創建新的命名空間,大家可以理解爲程序執行了5個同名的函數。
既然執行了5個函數,就有參數傳入和返回結果的過程。
我們先來看調用函數時,參數傳入的過程。
- 函數首次調用時,參數n的值爲5;
- 首次調用函數的return語句中,進行了第二次調用函數,並設置參數爲n-1;所以,在第二次調用的函數中,參數n的值變成了4;
- 以此類推,直至終止調用函數自身爲止。
接下來,我們再來看返回函數執行結果的過程。
- 程序調用5次函數的同時,進行了參數的傳入,第5次調用時,參數n的值是1,;此時,參數數值滿足n == 1的條件,不再繼續調用函數自身,通過return語句返回值,也就是1;
- 當1這個值被返回,程序回到了倒數第2次函數調用的return語句,此時語句中對函數的最後一次調用變成了具體的值(1),和變量n相乘之後,作爲返回值,再次返回給倒數第3次函數調用的return語句中;
- 以此類推,直至返回到首次調用的函數爲止。
所以,在我們剛纔的運行結果截圖中,我們能夠到,參數的顯示結果是5、4、3、2、1的順序(依次調用函數的順序);而返回值的顯示結果是1、2、6、24、120的順序(從最後一次調用函數向前返回計算結果的順序)。
所以,遞歸可以這麼理解,它就是函數循環調用函數自身;遞是指在循環調用的過程中,將參數遞進下一次函數的調用;歸是指當滿足終止循環調用函數的條件時,按照和調用時相反的順序,將函數的執行結果依次迴歸。
接下來,我們再來看一下冪的計算。
例如,2的4次方實際上是2*2*2*2的乘積。
示例代碼:
def power(x, y): # 定義函數計算參數x的y次方 if y == 1: # 滿足條件時終止函數循環調用 return x # 參數x的1次方返回參數x else: return x * power(x, y - 1) # 調用函數自身形成遞歸 print(power(2, 4)) # 顯示輸出結果爲:16
上方代碼中,當我們調用函數power時,函數開始循環調用函數自身,並且依次將參數y(3,2,1)傳入每次調用的函數中,當參數y爲1的時候,結束函數自身的循環調用,並且依次返回值(2,4,8,16)。
最後,我們再來看一下二分查詢的案例。
例如,有一個0~100的數字列表,從中查詢到一個指定的數字。
示例代碼:
def search(seq, number, lower, upper): # 定義函數,參數seq爲要查詢的序列,參數number爲要查詢的數字 # 參數lower爲查找區間的下限值,參數upper爲查找區間的上限值 if lower == upper: # 查找區間僅剩一個數字時,即找到查詢結果,停止循環調用 assert number != seq[upper], '這裏有問題!' # 如果不相等會拋出異常AssertionError(斷言錯誤),可以嘗試把"=="改爲"!=" print(str(lower)) # 顯示輸出結果爲:66 else: # 查找區間有多個數字時,繼續查找 middle = (lower + upper) // 2 # 通過整除計算,獲取查找區間數字的中間值 if number > seq[middle]: # 判斷查找的數字是否大於中間值 return search(seq, number, middle + 1, upper) # 如果大於中間值,中間值作爲查找區間下限值,繼續查找 else: return search(seq, number, lower, middle) # 如果小於中間值,中間值作爲查找區間上限值,繼續查找 search(range(0, 100), 66, 0, 100) # 顯示輸出結果爲:66
大家根據代碼中的註釋對遞歸過程進行理解,在此不再贅述。
不過在上方代碼中,我添加了一個assert關鍵字,此關鍵字爲斷言,可以幫助我們運行程序檢查語句中的錯誤。
使用格式爲:assert 表達式, ‘自定義的異常說明字符串’
自定義的異常說明字符串可以省略,未省略時,字符串和表達式之間必須用逗號(,)分隔。
如果斷言語句中的表達式出現錯誤,則會拋出AssertionError異常。
如果自定義了異常說明字符串,則會顯示“AssertionError:自定義的異常說明字符串”。
本節知識點:
1、函數的遞歸;
2、斷言的使用。
本節英文單詞與中文釋義:
1、recursion:遞歸
2、Infinite:無窮
3、maximum:最大值
4、depth:深度
5、exceeded:超過
6、factorial:階乘
7、search:查詢
8、power:冪
9、lower:下方
10、upper:上方
11、middle:中間
12、assert/assertion:異常
練習:根據給定的姓名,在列表中查詢姓名的序號(0-68)。
lst=[‘邢佳棟’,’李學慶’,’高昊’,’潘粵明’,’戴軍’,’薛之謙’,’賈宏聲’,’于波’,’李連杰’,’王斑’,’藍雨’,’劉恩佑’,’任泉’,’李光潔’,’姜文’,’黑龍’,’張殿菲’,’鄧超’,’張傑’,’楊坤’,’沙溢’,’李茂’,’黃磊’,’于小偉’,’劉冠翔’,’秦俊傑’,’張琳’,’陳坤’,’黃覺’,’邵峯’,’陳旭’,’馬天宇’,’楊子’,’鄧安奇’,’趙鴻飛’,’馬可’,’黃海波’,’黃志忠’,’李晨’,’後弦’,’王挺’,’何炅’,’朱亞文’,’胡軍’,’許亞軍’,’張涵予’,’賈乃亮’,’陸虎’,’印小天’,’於和偉’,’田亮’,’夏雨’,’李亞鵬’,’胡兵’,’王睿’,’保劍鋒’,’於震’,’甦醒’,’胡夏’,’張豐毅’,’劉翔’,’李玉剛’,’林依輪’,’袁弘’,’朱雨辰’,’丁志誠’,’黃徵’,’張子健’,’許嵩’]
答案:(見原文評論1樓)
轉載自:魔力 • Python » Python3萌新入門筆記(21)