一、什么是遞歸函數(shù)
(1)遞歸函數(shù)即自調用函數(shù),在函數(shù)內部直接或間接地自己調用自己,即函數(shù)的嵌套調用是函數(shù)本身。
從字面上來看遞歸,遞即遞推,采用循環(huán)的思路來描述復雜問題的方法。在遞推階段每一個遞歸調用通過進一步調用自己來記住這次遞歸過程,當其中調用滿足終止條件時,遞推結束。歸即回歸,函數(shù)調用已逆序的方式回歸,知道最初調用的函數(shù)返回為止,此時遞歸過程結束。舉個例子:
(2)遞歸的基本原理
第一:每一級的函數(shù)調用都有自己的變量。
第二:每一次函數(shù)調用都會有一次返回。
第三:遞歸函數(shù)中,位于遞歸調用前的語句和各級被調用函數(shù)具有相同的執(zhí)行順序。
第四:遞歸函數(shù)中,位于遞歸調用后的語句的執(zhí)行順序和各個被調用函數(shù)的順序相反。
第五:雖然每一級遞歸都有自己的變量,但是函數(shù)代碼并不會得到復制。
最后:遞歸函數(shù)中必須包含可以終止遞歸調用的語句。
(3)遞歸的優(yōu)缺點
其優(yōu)點在于為某些變成問題提供了最簡單的解決方法,簡化了程序設計。而缺點是一些遞歸算法會很快耗盡計算機的內存資源。同時,使用遞歸的程序難于閱讀和維護。
二、函數(shù)調用機制的說明
任何函數(shù)之間不能嵌套定義,調用函數(shù)與被調用函數(shù)之間相互獨立遞歸函數(shù)的概念用法與實例(彼此可以調用)。發(fā)生函數(shù)調用時,被調用函數(shù)中保護了調用函數(shù)的運行環(huán)境和返回地址,使得調用函數(shù)的狀態(tài)可以被調用函數(shù)運行返回完全恢復,而且該狀態(tài)與被調用函數(shù)無關。
被調用函數(shù)運行的代碼雖然是同一個函數(shù)的代碼體,但由于調用點,調用時狀態(tài),返回點的不同,可以看作是函數(shù)的一個副本,與調用函數(shù)的代碼無關,所以函數(shù)的代碼是獨立的。被調用函數(shù)運行的棧空間獨立于調用函數(shù)的棧空間,所以與調用函數(shù)之間的數(shù)據(jù)也是無關的。函數(shù)之間考參數(shù)傳遞和返回值來聯(lián)系,函數(shù)看作為黑盒。
這種機制決定了 C/C++ 允許函數(shù)遞歸調用。
上面這段話的意思可以理解為,遞歸函數(shù)調用自身函數(shù)的時候,可以看作調用的是調用別的函數(shù)。
再有,被調用函數(shù)運行的棧空間獨立于調用函數(shù)的棧空間 這句話如何理解?
首先,我們先說一下棧的概念:
參看:遞歸函數(shù)的概念用法與實例
參看:C語言再學習 -- 內存管理
棧是一個后進先出(LIFO)的壓入(push)和彈出(pop)式的數(shù)據(jù)結構。在程序運行時,系統(tǒng)每次向棧中壓入一個對象,然后棧指針向下移動一個位置。當系統(tǒng)從棧中彈出一個對象時,最近進棧的對象將被彈出。然后棧指針向上移動一個位置。程序員經常利用棧這種數(shù)據(jù)結構來處理那些最適合用后進先出邏輯來描述的編程問題。這里討論的程序中的棧每個程序中都是存在的,它不需要程序員編寫代碼去維護,而是由運行時系統(tǒng)自動處理。所謂系統(tǒng)自動維護,實際上就是編譯器所產生的程序代碼。盡管在程序中看不到他們,但是程序員應該對此有所了解。
再看看程序中的棧是如何工作的。當一個函數(shù)(調用者)調用另一個函數(shù)(被調用者)時,運行系統(tǒng)將把調用者的所有實參和返回地址壓入棧中,棧指針將移到合適的位置來容納這些數(shù)據(jù)。最后進棧的是調用者的返回地址。當被調用這開始執(zhí)行時,系統(tǒng)把被調用者的自變量壓入棧中,并把棧指針再向下移,以保證有足夠的空間存儲被調用這聲明的所有自變量。當調用這把實參壓入棧后,被調用者就在棧中以自變量的形式建立形參。被調用這內部的其他自變量也是存放在棧中的。由于這些進棧操作,棧指針已經移動所有這些局部變量之下。但是被調用者記錄了它剛開始執(zhí)行時的初始棧指針,以它為參考,用正或負的偏移量來訪問棧中的變量。當被調用者準備返回時,系統(tǒng)彈出棧中所有的自變量,這是棧指針移動了被調用者剛開始執(zhí)行時的位置。接著被調用者返回,系統(tǒng)從棧中彈出返回地址,調用者就可以繼續(xù)執(zhí)行了。當調用者繼續(xù)執(zhí)行時,系統(tǒng)還將從棧中彈出調用者的實參,于是棧指針回到了調用發(fā)生前的位置。
現(xiàn)在接著說遞歸:
上面有提到了函數(shù)調用機制。遞歸之所以能實現(xiàn),是因為函數(shù)的每個執(zhí)行過程都在棧中有自己的形參和局部變量的拷貝,這些拷貝和函數(shù)的其他執(zhí)行過程毫不相干。這種機制是當代大多數(shù)程序設計語言實現(xiàn)子程序結構的基礎,是使得遞歸成為可能。假定某個調用函數(shù)調用了一個被調用函數(shù),再假定被調用函數(shù)又反過來調用了調用函數(shù)。這第二個調用就被稱為調用函數(shù)的遞歸,因為它發(fā)生在調用函數(shù)的當前執(zhí)行過程運行完畢之前。而且,因為這個原先的調用函數(shù)、現(xiàn)在的被調用函數(shù)在棧中較低的位置有它獨立的一組參數(shù)和自變量,原先的參數(shù)和變量將不受影響,所以遞歸能正常工作。程序遍歷執(zhí)行這些函數(shù)的過程就被稱為遞歸下降。
程序員需保證遞歸函數(shù)不會隨意改變靜態(tài)變量和全局變量的值,以避免在遞歸下降過程中的上層函數(shù)出錯。程序員還必須確保有一個終止條件來結束遞歸下降過程,并且返回到頂層。看下面的例子:
這里有一個問題一定要注意,就是static int sum = 0;
有些人就不明白,為什么要使用 static 類型修飾符,為什么不使用 int sum=0;?如果使用 int sum=0; 這樣的語句,在每次調用函數(shù)add()的時候,sum的值都是賦值為0,也就是第一步雖然加了1上來,可是第二次調用的時候,sum又回到了0。我們前面說了,static能保證本次初始化的值是上次執(zhí)行后的值,這樣也就保證了前面想加的結果不會丟失。如果你修改為int sum=0,最后結果一定是最后結果是5而不是15。
上面的例子就很好的解釋了,被調用函數(shù)運行的棧空間獨立于調用函數(shù)的棧空間 這句話。
三、遞歸調用的形式
遞歸函數(shù)有直接遞歸調用和間接調用兩種形式.
直接遞歸即在函數(shù)出現(xiàn)調用函數(shù)本身,例如:
間接遞歸調用指函數(shù)中調用其他函數(shù),而該其他函數(shù)卻又調用了本函數(shù)。例如:
下面,再擴展一種遞歸,尾遞歸
當遞歸調用是整個函數(shù)中最后執(zhí)行的語句且它的返回值不屬于表達式的一部分時,這個遞歸調用就是尾遞歸的。
上面的直接遞歸例子的返回值是 return num*func(num-1); 是一個表達式自然不是尾遞歸了,將其改為尾遞歸:
當編譯器檢查到一個函數(shù)是尾遞歸的時候,它就會覆蓋當前活躍記錄而不是在棧中去創(chuàng)建一個新的,這樣就解決了普通遞歸函數(shù)占用棧空間過大的問題。遺憾的是,大多數(shù)編程語言沒有針對尾遞歸做優(yōu)化,所以,即使把函數(shù)改成尾遞歸方式,也會導致棧溢出。星辰就暫時把遞歸說到這,下一篇的話繼續(xù)接上,內容還是講遞歸吧,基于太多,一下子寫太多的話,各位看了也會累。