一、什麼是遞歸
遞歸算法,就是直接或間接調用自身的函數,也就是把一個大的複雜的問題層層轉換爲一個小的和原問題相似的問題來求解的這樣一種策略。
上面解釋可能有點太官方了,來看看知乎上大神的通俗易懂的解釋:
解釋一:
“古之慾明明德於天下者,先治其國;欲治其國者,先齊其家;欲齊其家者,先修其身;欲修其身者,先正其心;欲正其心者,先誠其意;欲誠其意者,先致其知,致知在格物。物格而後知至,知至而後意誠,意誠而後心正,心正而後身修,身修而後家齊,家齊而後國治,國治而後天下平。”
這是一個調用自身的過程,我們把”明德於天下“當作函數本身來理解,每一層調用的參數依次是治國、齊家、修身、正心、誠意、致知、格物。最後在”格物“觸發返回條件。
解釋二:
天下有奇族人姓計,長生不老。一日其孫問其父:吾之18代祖名何?
其父不明,父問其父
其父不明,父問其父
其父不明,父問其父
其父不明,父問其父
…
晌後,其18代祖回其子:你猜
然其回其子:你猜
然其回其子:你猜
然其回其子:你猜
然其回其子:你猜
……
終,計姓末代孫知其18代祖名“你猜”
解釋三:
假設你在一個電影院,你想知道自己坐在哪一排,但是前面人很多,你懶得去數了,於是你問前一排的人「你坐在哪一排?」,這樣前面的人 (代號 A) 回答你以後,你就知道自己在哪一排了——只要把 A 的答案加一,就是自己所在的排了。不料 A 比你還懶,他也不想數,於是他也問他前面的人 B「你坐在哪一排?」,這樣 A 可以用和你一模一樣的步驟知道自己所在的排。然後 B 也如法炮製。直到他們這一串人問到了最前面的一排,第一排的人告訴問問題的人「我在第一排」。最後大家就都知道自己在哪一排了。
二、遞歸特點
遞歸算法解決問題的特點:
- 遞歸就是方法裏調用自身。
- 在使用遞增歸策略時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。
- 遞歸算法解題通常顯得很簡潔,但遞歸算法解題的運行效率較低。所以一般不提倡用遞歸算法設計程序。
- 在遞歸調用的過程當中系統爲每一層的返回點、局部量等開闢了棧來存儲。遞歸次數過多容易造成棧溢出等,所以一般不提倡用遞歸算法設計程序。
遞歸一般過程:
循環、迭代、遍歷和遞歸的區別:
- 循環(loop),指的是在滿足條件的情況下,重複執行同一段代碼。比如,while語句。
循環則技能對應集合,列表,數組等,也能對執行代碼進行操作。 - 迭代(iterate),指的是按照某種順序逐個訪問列表中的每一項。比如,for語句。
迭代只能對應集合,列表,數組等。不能對執行代碼進行迭代。 - 遍歷(traversal),指的是按照一定的規則訪問樹形結構中的每個節點,而且每個節點都只訪問一次。
遍歷同迭代一樣,也不能對執行代碼進行遍歷。 - 遞歸(recursion),指的是一個函數不斷調用自身的行爲。比如,以編程方式輸出著名的斐波納契數列
三、遞歸例子
1、N的階乘
遞歸版:
int factorial(int index)
{
return index==1?1:index*factorial(index - 1);
}
非遞歸版:
int factorial_norecur(int index)
{
int result=1;
while(index>0)
{
result*=index;
index-=1;
}
return result;
}
遞歸過程(比如5的階乘):
其實也就是如下過程:
factorial(5)
5*factorial(4)
5*(4*factorial(3))
5*(4*(3*factorial(2)))
5*(4*(3*(2*factorial(1)))))
5*(4*(3*(2*1)))
5*(4*(3*2))
5*(4*6)
5*24
120
2、1到N的和
int nsum(int n)
{
return n==0?0:n+factorial(n- 1);
}
3、斐波拉契
1、遞歸實現
使用公式f[n]=f[n-1]+f[n-2],依次遞歸計算,遞歸結束條件是f[1]=1,f[2]=1。
int fib1(int index) //遞歸實現
{
if(index<1)
{
return -1;
}
if(index==1 || index==2)
return 1;
return fib1(index-1)+fib1(index-2);
}
2、數組實現
空間複雜度和時間複雜度都是0(n),效率一般,比遞歸來得快。
int fib2(int index) //數組實現
{
if(index<1)
{
return -1;
}
if(index<3)
{
return 1;
}
int *a=new int[index];
a[0]=a[1]=1;
for(int i=2;i<index;i++)
a[i]=a[i-1]+a[i-2];
int m=a[index-1];
delete a; //釋放內存空間
return m;
}
3、vector實現
時間複雜度是0(n),時間複雜度是0(1),就是不知道vector的效率高不高,當然vector有自己的屬性會佔用資源。
int fib3(int index) //借用vector<int>實現
{
if(index<1)
{
return -1;
}
vector<int> a(2,1); //創建一個含有2個元素都爲1的向量
a.reserve(3);
for(int i=2;i<index;i++)
{
a.insert(a.begin(),a.at(0)+a.at(1));
a.pop_back();
}
return a.at(0);
}
4、queue實現
當然隊列比數組更適合實現斐波那契數列,時間複雜度和空間複雜度和vector一樣,但隊列太適合這裏了,
f(n)=f(n-1)+f(n-2),f(n)只和f(n-1)和f(n-2)有關,f(n)入隊列後,f(n-2)就可以出隊列了。
int fib4(int index) //隊列實現
{
if(index<1)
{
return -1;
}
queue<int>q;
q.push(1);
q.push(1);
for(int i=2;i<index;i++)
{
q.push(q.front()+q.back());
q.pop();
}
return q.back();
}
5、迭代實現
迭代實現是最高效的,時間複雜度是0(n),空間複雜度是0(1)。
int fib5(int n) //迭代實現
{
int i,a=1,b=1,c=0;
if(n<1) return -1;
else if(n<2) return 1;
for(i=2;i<n;i++)
{
c=a+b; //輾轉相加法(類似於求最大公約數的輾轉相除法)
a=b;
b=c;
}
return c;
}
6、公式實現
斐波那契數列有公式的,所以可以使用公式來計算的。
int fib6(int n)
{
double gh5=sqrt((double)5);
return (pow((1+gh5),n)-pow((1-gh5),n))/(pow((double)2,n)*gh5);
}
對於斐波拉契有很多變種,比如跳臺階,共n個臺階,每次最多兩步,有多少種跳法。我們知道最後那個臺階有兩種選擇可到達,要不從n-1臺階跳一步,要不從n-2臺階跳兩步,那麼可得到f[n]=f[n-1]+f[n-2]。同理,如果每次最多跳三步,那麼就是f[n]=f[n-1]+f[n-2]+f[n-3]。
4、求兩個數的最大公約數
long gcd(int a,int b) //遞歸版
{
if(a%b==0)
return b;
return gcd(b,a%b);
}
long gcd_norecur(int a,int b) //非遞歸版
{
int temp;
while(b!=0)
{
temp=a%b;
a=b;
b=temp;
}
return a;
}
四、遞歸與非遞歸的轉化
遞歸是指某個函數或過程直接或間接的調用自身。一般地一個遞歸包括遞歸出口和遞歸體兩部分,遞歸出口確定遞歸到何時結束,而遞歸體確定遞歸求解時的遞推關係。
遞歸算法有兩個基本特徵:一是遞歸算法是一種分而治之的、把複雜問題分解爲簡單問題的求解問題方法,對於求解某些複雜問題,遞歸算法分析問題的方法是有效地;而遞歸算法的時間、控件效率通常比較差。因此對解決某些問題時,我們希望用遞歸算法分析問題,用非遞歸算法解決問題,這就需要把遞歸算法轉換爲非遞歸算法。
把遞歸算法轉化爲非遞歸算法有如下三種基本方法:
- 通過分析,跳過分解過程,直接用循環結構的算法實現求解過程。比如N的階乘,斐波拉契,最大公約數
- 自己用棧模擬系統的運行時棧,通過分析只保存必須保存的信息,從而用非遞歸算法替代遞歸算法。比如樹的遍歷。