什么是遞歸?
遞歸,就是函數在運行的過程中調用自己。
代碼示例
def recursion(n): print(n) recursion(n+1) recursion(1)
出現的效果就是,這個函數在不斷的調用自己,每次調用就n+1,相當於循環了。

可是為何執行了900多次就出錯了呢?還說超過了最大遞歸深度限制,為什么要限制呢?
通俗來講,是因為每個函數在調用自己的時候 還沒有退出,占內存,多了肯定會導致內存崩潰。
本質上講呢,在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。
遞歸的特點
讓我們通過現象來看本質, 下面是是用遞歸寫的,讓10不斷除以2,直到0為止。

打印的是1, 然后最里層的函數就結束了,結束后會返回到之前調用它的位置。即上一層,上一層打印的是2,再就是5,再就是10,即最外層函數,然后結束,總結,這個遞歸就是一層層進去,還要一層層出來。

通過上面的例子,我們可以總結遞歸幾個特點:
- 必須有一個明確的結束條件,要不就會變成死循環了,最終撐爆系統
- 每次進入更深一層遞歸時,問題規模相比上次遞歸都應有所減少
- 遞歸執行效率不高,遞歸層次過多會導致棧溢出
遞歸有什么用呢?
可以用於解決很多算法問題,把復雜的問題分成一個個小問題,一一解決。
比如求斐波那契數列、漢諾塔、多級評論樹、二分查找、求階乘等。用遞歸求斐波那契數列、漢諾塔 對初學者來講可能理解起來不太容易,所以我們用階乘和二分查找來給大家演示一下。
求階乘
任何大於1的自然數n階乘表示方法:
n!=1×2×3×……×n
或
n!=n×(n-1)!
即舉例:4! = 4x3x2x1 = 24
用遞歸代碼來實現
def factorial(n): if n == 0: #是0的時候,就運算完了 return 1 return n * factorial(n-1) # 每次遞歸相乘,n值都較之前小1 d = factorial(4) print(d)
2分查找
在一個已排序的數組data_set中,使用二分查找n,假如這個數組的范圍是[low...high],我們要的n就在這個范圍里。查找的方法是拿low到high的正中間的值,我們假設是mid,來跟n相比,如果mid>n,說明我們要查找的n在前數組data_set的前半部,否則就在后半部。無論是在前半部還是后半部,將那部分再次折半查找,重復這個過程,知道查找到n值所在的地方。
data_set = list(range(101)) def b_search(n,low,high,d): mid = int((low+high)/2) # 找到列表中間的值 if low == high: print("not find") return if d[mid] > n: # 列表中間值>n, 代數要找的數據在左邊 print("go left:",low,high,d[mid]) b_search(n,low,mid,d) # 去左邊找 elif d[mid] < n: # 代數要找的數據在左邊 print("go right:",low,high,d[mid]) b_search(n,mid+1,high,d) # 去右邊找 else: print("find it ", d[mid]) b_search(188, 0,len(data_set),data_set)
go right: 0 101 50 go right: 51 101 76 go right: 77 101 89 go right: 90 101 95 go right: 96 101 98 go right: 99 101 100 not find
最多將會操作7次,其實因為每一次我們都拋掉當前確定的區間的一半的區間作為不可能解部分,那么相當於求最多操作次數,就是在區間內,最多將有多少個一半可以拋去、那么就是將100一直除以2,直到不能除為止。
那么這個運算過程,其實就是相當於求了一個log2(100)≈7。
補充:
在講特性時,我們說遞歸效率不高,因為每遞歸一次,就多了一層棧,遞歸次數太多還會導致棧溢出,這也是為什么python會默認限制遞歸次數的原因。但有一種方式是可以實現遞歸過程中不產生多層棧的,即尾遞歸,
尾遞歸
在函數最尾部有return,return值是遞歸形式調用,且返回值與上一層函數無任何依賴。
尾遞歸例子
def calc(n): print(n - 1) if n > -50: return calc(n-1)
我們之前求的階乘是尾遞歸么?
def factorial(n): if n == 0: #是0的時候,就運算完了 return 1 return n * factorial(n-1) # 每次遞歸相乘,n值都較之前小1 d = factorial(4) print(d)
上面的這種遞歸計算最終的return操作是乘法操作。所以不是尾遞歸。因為每個活躍期的返回值都依賴於用n乘以下一個活躍期的返回值,因此每次調用產生的棧幀將不得不保存在棧上直到下一個子調用的返回值確定。
