遞歸(Recursion)簡述及一些註意事項
《Data Structure and Algorithm Analysis in C++》筆記
大多數的數學函數可以被描述成簡單表達式。
例如:
華氏度和攝氏度轉換的表達式為
C = 5 *( F - 32 ) / 9
這種式子我們可以明確地一行行轉換成C++代碼。
但有時候,數學函數的表達式采用另一種形式——遞推式(iterative)。
例如:
f(0) = 0
f(x) = 2 * f(x - 1) + x2
計算幾項結果:
f(1) = 1,f(2) = 6, f(3) = 21 ...
遞推式通常給出初值和條件,後項由前項加以條件得到結果,層層進行以得到各項。
對於遞推式,C++語言可以通過兩種思路處理。
其中一種是遞歸(recursive)。
C++允許函數被遞歸式定義,但這種允許是有條件的。
並非所有數學上的遞推式都可以高效地(或者正確地)使用C++模擬。
出於效率考慮,我們理想中的遞歸實現應當保證後項依賴的前項不涉及復雜計算。
上述遞推式的C++語言描述:
int f(int x) {
if (x == 0) {
return 0;
}
else {
return 2 * f(x - 1) + x*x;
}
}
其中第2到3行是遞推式f(0)的描述,它在遞推式中叫做初始條件,而在遞歸實現中稱為遞歸出口
僅僅聲明f(x) = 2f(x-1) +x2的關系是無意義的,遞歸實現必須先給出明確的遞歸出口。
對於遞歸,通常會存在普遍的疑問。
其中一個共同的問題就是:難道這不會陷入繞圈邏輯?如何避免呢?
答案是:
只要我們在給定遞推關系時,保證遞推關系僅僅依賴於前後項而非自身即可。
換句話:
遞推f(5)需要用到計算f(5)的結果,那將會陷入繞圈。
但遞推f(5)僅需要用到計算f(4)的結果,不會繞圈。
另外一個重要問題,檢查遞歸出口有效性。(小心負數,小心除法)
在上述遞歸實現中,
想知道f(3),就得知道f(2)。想知道f(2),就得知道f(1)。想知道f(1),就得知道f(0)
這使得其看上去像是個美妙的遞歸實現。
小心負數:
但假如我們試圖計算符合數據類型的f(-1)呢?
想知道f(-1),就得知道f(-2)。想知道f(-2),就得知道f(-3)。……
以此類推,想得到前項需要用到無限的後項,計算機永遠也得不到結果,這種實現明顯是不合格的。
另外的導致遞推出口失效的隱秘可能,除法運算:
int bad(int n) {
if (n == 0) {
return 0;
}
else {
return bad(n / 3 + 1) + n - 1;
}
}
它看上去定義了明確的遞推出口,但bad(1) 需要知道bad(n / 3 + 1),而C++的除法運算會隱式轉換,1/3 =0.
bad(1)需要知道bad(1),bad(2)需要知道bad(1),而bad(3)到bad(5)都需要知道bad(2)。
而這種混亂關系中,我們僅僅知道bad(0) = 0這個無法抵達的出口,因此整個遞歸實現都失效了。
在遞推關系涉及到負數和除法運算時,我們要格外小心。
遞歸(Recursion)簡述及一些註意事項