---
# System prepended metadata

title: 演算法與資料結構 遞迴簡述
tags: [Algorithm]

---

# 演算法與資料結構 遞迴簡述
###### tags: `Algorithm` 
我們所熟悉的數學函式大多數都是由一行簡單的方程式所構成，以華氏溫度轉換成攝氏溫度為例子
> $C =5(F - 32)/9$

多數情況下只要我們有了式子，就可以簡單地寫出我們所需要的程式進行運算

但有一些數學運算式卻不是如此定義的，舉一個例子，有一個F函數，其定義域屬於非負整數集合，滿足以下情況
> $F(0) = 0$
> $F(X) = 2F(X - 1) + X^2$

從這個定義中我們可以發現，F(1) = 1, F(2) = 6, F(3) = 21，當一個函數使用自身函數作為定義時，就可以被稱作為遞迴(recursive)

而上方式子的舉例，使用C語言進行實作，我們可以表示為以下
```c=
int F(int x)
{
    if(X == 0)
    {
        return 0;
    }
    else
    {
        return 2 * F(X - 1) + X * X;
    }
}
```
而第三行和第四行我們所處理的，就是所謂的基準情況(Base case)，此時函數的值可以直接求出而不需要透過遞迴。

而 $F(X) = 2F(X - 1) + X^2$ 若失去了基本情況的假設，那麼便失去了意義，也就是一個遞迴若失去了基本情況，則遞迴本身不具有任何意義。

我們在透過遞迴求出最終答案時，每一個子問題最終需要回到基本情況，才能得出結果。

## 錯誤的遞迴使用，無效輸入
而我們設想一個情況，假設我們設X = -1時，丟入函式進行處理時，會發生甚麼樣的狀況

F(-1)會去呼叫F(-2)，F(-2)會去呼叫F(-3)，我們發現會永遠無法觸發基本情況的發生，最終導致不斷的呼叫，直到記憶體被占滿空間(Stack Overflow)。

而對於這樣的無效輸入，我們必須將其過濾，避免出現程式崩潰的情況發生

## 錯誤的遞迴使用，無效的定義
以下為一個遞迴程式
```c=
int Error(unsigned int N)
{
    if(N == 0)
    {
        return 0;
    }
    else
    {
        return Error( N / 3 + 1) + N - 1;
    }
}
```
我們仔細檢視第9行的式子 $Error( N / 3 + 1) + N - 1$ ，當我們呼叫Error(1)時，回傳的結果為Error(1)，這一件事情導致所有呼叫到Error(1)的函式都無法求得值。在遞迴程式設計時，需要注意其終止條件，避免程式崩潰，或是無法預期的錯誤產生。

## 列印數字
假設我們輸入一個正整數N，我們使用一個函式PrintOut(N)，並且這個函式對於印出數字，只能呼叫PrintDigit，這個函式一次只能印出一個數字，而這個問題，我們有一個非常簡單的解法，假設要印出 12345 ，我們先印出1234，然後在印出5，對於印出5，我們可以很簡單的使用PrintDigit(N % 10)處理，剩下的問題我們可以透過遞迴進行處理。

但這只處理了一般的情況，我們仍然需要確保這個程式在面對所有情況下，都可以觸發基本情況。假設0 <= N <= 10，而我們的基本情況就是PrintDigit(N)，這麼一來，我們便處理完畢所有情況。
```c=
void PrintOut(unsigned int N)
{
    if(N >= 10)
    {
        PrintOut(N / 10);
    }
    PrintDigit(N % 10);
}
```
而這個程式，其實還可以再進行優化，將N % 10優化為N - [N / 10] * 10，原因在於mod的操作相當耗費系統資源。


## 遞迴與歸納法，證明我們可以相信遞迴
我們使用歸納法對於上述列印數字給出嚴謹的證明

定理:
對於所有N >= 0，數字的遞迴列印是正確的

證明:
當N只有一位數字時，此程式必然正確，只呼叫了PrintDigit。
設PrintOut對所有N在對所有k位數，k屬於任意有理數，為正確的。

k + 1位數字可以透過前k位數字和一位最低位數字表示，前k數字恰等於N / 10，由於歸納法假設前k位數字可以被正確的列印出來，而最後一位數為N % 10，因此可以列印出任意k + 1位數
由數學歸納法，此證明完畢。

回想一下高中所學的數學歸納法，就會發現這個證明雖然看起來很奇怪，卻十分的正確，事實上他就是對於這個演算法的描述。

他描述的即為這個演算法的行為，在同一個問題中，所有子問題都可以假設執行正確，遞迴只需把所有的仔問題結果進行結合形成現行問題的解。而我們之所以確保他是正確的，其根據即是數學歸納法，而數學歸納法屬於完全嚴謹的演繹推理法，因此可以相信他是正確的

而根據上述，我們可以給出遞迴的第三個法則。

3.設計法則(design rule): 假設所有的遞迴呼叫都可以執行。而這是一個重要的法則，因為有了這一條法則，我們在設計遞迴程式設計時，不需要知道其中的細節，不必追蹤遞迴的細部呼叫。

## 遞迴的主要問題
遞迴主要的問題是記憶體空間的使用，雖這些記憶體使用十分合理，且可以幫助我們寫出更加簡潔的程式碼，但遞迴絕不應該是作為簡單for迴圈的替代物，之後我們將會討論遞迴涉及的記憶體空間的使用。

## 結論，遞迴設計準則
當我們在設計遞迴程式時，應該考慮以下四個基本原則
1. 基準情況(Base case): 必須存在某一些情況，他不需要經過遞迴就可以求出其結果
2. 不斷推進(making progress): 對於那些需要經由遞迴求得解的情況，每一次的遞迴呼叫必須要使求解的過程項基本情況推進。
3. 設計法則(Design rule): 假設所有遞迴呼叫都可以執行
4. 合成效益法則(compound interest rule): 在求解一個問題時，若子問題已經在先前的遞迴呼叫中求出解，不要在遞迴呼叫中不斷重複計算(可使用動態規劃解決)。



