想起上大學那會兒遞歸調用曾是那麼使人頭痛,如今工做也近兩年時間了,對遞歸卻是有了較明晰的瞭解.
遞歸,數學裏面叫recursion,其實就是遞推關係. 中學數學有一部分其實就是遞歸的很是典型的作法,不過老師們都沒怎麼擴展,新課標必修五第二章數列應該算是咱們第一次接觸遞推的概念了.
其實說到遞歸,大夥都知道就是本身調本身,這樣其實你們都明白,可是說來怎麼調?如何控制?又如何看獲得結果是想要的呢?相信仍是很暈,下面從中學數學裏面來看看吧.
第一部分、兩個典型的例子,等差數列與等比數列
其實這其實是一個例子,什麼是等差數列?就是後一項總比前一項多一個數,這個數不變...,也有的說:「就是後一項減去前一項爲必定常數...」
因此咱們常常用表達式表示爲
a(n)=a(n-1)+d\]
那又有問題了,這裏肯定了一個數列沒有呢?固然沒有,我說後一項比前一項多2,這是什麼數列?
是1,3,5,7,9, ...仍是2,4,6,8,10, ...
固然弄不清楚,爲何呢?由於咱們談到的數列須要有一個首項,即第一項的值,因此一旦談到解遞推數列,就應該有兩個內容,一個是連續項間的關係,另外一個就是首項關係.
那麼就能夠利用疊加的方法來計算了:
假設這裏首項爲1,也就是a(1)=1,而這個常數就爲2
那麼
a(n)=a(n-1)+2
a(n-1)=a(n-2)+2
a(n-2)=a(n-3)+2
a(n-3)=a(n-4)+2
...
a(3)=a(2)+2
a(2)=a(1)+2
將等號左邊的依次相加,右邊的也依次相加
這樣很容易發現,左邊有一部分從a(2)一直加到a(n-1),右邊也有一部分從a(2)一直加到a(n-1),那麼消去,就左邊剩下a(n),右邊就剩下a(1)和n-1個2相加了,數學公式就成了
a(n)=a(1)+2(n-1)
即
a(n)=1+2(n-1)
就是等差數列的通向公式啦,那麼這個遞推關係中能夠看到a(n)=a(n-1)+x即爲連續項間的關係,而a(1)=1就是首項啦. 那這個和遞歸間的關係就很明顯啦
a(n)=a(n-1)+x表示,調用a(n)即執行a(n-1)+x,那麼同時調用a(n-1),...這樣一直下去,最後當a(1)時不在調用函數,直接返回1,再一路加回去,結束遞歸.
下面看看用代碼來實現,仍是假定首項爲1,公差爲2
#include <stdio.h>
int main()
{
int AddFun(int);
int num;
printf("Please input num:");
scanf("%d",&num);
printf("The result is %d",AddFun(num));
return 0;
}
int AddFun(int n)
{
if(n == 1)
{
return 1;
}
return AddFun(n-1) + 2;
}
這裏呢AddFun(n)就是以前看到的a(n),在調用的過程當中先判斷n是否爲1,若是爲1固然就表示如今是首項了,不在繼續調用,直接返回結果1
這裏使用的不是數學中的公式計算,純粹的事利用遞推關係獲得的結果.
再看看等比數列. 相信已經很清楚了,等比數列就是後一項是前一項的定常數被,表示爲
a(n)=a(n-1)*q
LaTeX代碼爲
\[a_n=a_{n-1}\cdot q\]
將函數改一改就能使用了,假設首項爲1,公比爲2:ios
static void Main(string[] args) { Console.Write("Please input num:"); int n = int.Parse(Console.ReadLine()); int result = AddFun(n); Console.WriteLine(result); Console.ReadKey(); } private static int AddFun(int n) { if (n == 1) { return 1; } else { return AddFun(n - 1) * 2; } }
固然遞歸不必定要調用函數開完成,使用for循環同樣能夠作到編程
第二部分、遞歸調用注意(直接遞歸)函數
遞歸調用在編程裏面就是對一個函數,或方法進行本身掉本身,那麼在編寫遞歸代碼時有三點要注意,首先要注意一個問題,就是遞推關係,這裏須要抽象出一個遞推的關係,不必定只有兩層,或許是三層甚至更多
例如:樓梯有n階臺階,上樓能夠一步上1階,也能夠一步上2階,編一程序計算共有多少種不一樣的走法.
先來分析一下,若是隻有一個臺階,那麼顯然只有1種,因此a(1)=1
若是有兩個臺階,呢麼就有每次上一層,上兩次,和一次上兩層,即1+1和2共2種,即a(2)=2
當n=3時,那麼就有1+1+一、1+二、2+1共3種了,彷佛還看不出規律,那麼分析一下遞推關係,即先後項間的關係
到達第三層有兩個方法,一是從第二層走一步到第三層,那麼有a(2)種方法,又能夠從第一層走兩步到第三層,即a(1)種方法,拍腦殼了,顯然到達第三層有a(1)+a(2)種方法,那麼是否是呢?驗證一下
a(1)+a(2)=1+2=3,咦,正好,看來有點像了,再來看看第四層是否是
當n=4時,按照上邊的分析確定爲a(2)+a(3)=2+3=5,下面咱們枚舉一下:1+1+1+一、1+1+二、1+2+一、2+1+一、2+2,剛恰好,看來就是她了!
那麼遞推關係就有了,即a(n)=a(n-1)+a(n-2),同時還有了首項的值:a(1)=1,a(2)=2,因爲遞推關係有三項,很顯然必須有兩個初始值
好啦,上面討論了遞歸的一個注意,下面是第二個注意,臨界條件
既然遞歸就是本身調本身,那麼怎麼中止呢,顯然不作邏輯判斷,計算機會一直運行下去,知道程序崩潰(棧溢出),因此就須要加上一個if判斷來跳出遞歸,其實就是前面提到的數列首項,就是這裏的a(1)和a(2)
那麼代碼就能夠這麼寫了:
spa
private static int Count(int n) { if (n == 2) { return 2; } else if (n == 1) { return 1; } else return Count(n - 1) + Count(n - 2); }
實際上這就是一個典型的Fibonacci數列,只是初始值不一樣罷了。
如今剩下第三個要注意的事項了,就是遞歸體.
先看兩個函數代碼(C++代碼):code
void Test_1(int n) { cout<<n<<endl; if(n < 6) { Test_1( n + 1 ); } } 2、 void Test_2(int n) { if(n < 6) { Test_2( n + 1 ); } cout<<n<<endl; }
若是在main()函數中分別調用這兩個函數,會有什麼執行結果呢?
如今咱們假定n爲0
第一個函數打印出
0
1
2
3
4
5
6
第二個函數打印出
6
5
4
3
2
1
0
這個就很奇怪了,這是爲何呢?
實際上前面咱們考慮的遞歸關係是單一的遞歸,可是這兩個遞歸體中,除了完成遞歸外,還有其餘執行代碼.
其實以前也有,只是沒有顯示出來,就沒有被察覺而已
那麼怎麼去分析呢?
其實很簡單,遞歸體自己可分爲三個部分,一個是頭代碼,一個是遞歸表達式,一個是尾代碼,區分頭尾的天然就是遞歸表達式了
看這段代碼:blog
#include <iostream> using namespace std; int main() { void Test(int); Test(1); return 0; } void Test(int n) { cout<<n<<endl; if(n < 3) { Test( n + 1 ); cout<<n<<endl; } }
輸出結果爲
1
2
3
2
1
那麼在這段代碼中if()前面的輸出流爲頭代碼,中間的Test( n + 1 )爲遞歸表達式,這裏的尾代碼依然是一個輸出流
那麼分析此程序怎麼作呢?很簡單,八個字
「頭尾分開,等待歸來」
頭尾分開,就是將兩個cout<<n<<endl分開,第一個cout<<n<<endl執行,而後轉入第一次遞歸,等待遞歸結束後回到第二個cout<<n<<endl語句執行
這樣說很抽象,咱們一步步分析一下
第一次調用,n=1
執行頭cout<<n<<endl,輸出1,而且尾cout<<n<<endl等待,其值也是1,但未輸出;
進入第一次遞歸,n=2
執行頭cout<<n<<endl,輸出2,而且尾cout<<n<<endl等待,其值是2,但未輸出;
進入第二次遞歸,n=3
執行頭cout<<n<<endl,輸出3,但判斷n < 3 不成立,即再也不執行尾cout<<n<<endl語句,函數體結束
回到第一次遞歸體,執行等待的尾cout<<n<<endl,輸出2,第一次遞歸體結束,會帶最外層
執行等待的cout<<n<<endl,輸出1,整個函數調用結束,回到main()函數體繼續執行其餘命令.
形象的說就像是一架手風琴同樣,函數調用開始就拉開手風琴,而後進入遞歸,等待內層遞歸結束才執行後面的代碼
也就八個字「頭尾分開,等待歸來」
其實這就是實現的棧的結構
到這裏就基本結束了我所理解的遞歸含義,總結一下:
遞歸調用即遞推關係
遞歸三個注意:注意遞推表達、注意臨界判斷、注意遞歸體遞歸