遞歸算法的優化


/**
* 遞歸計算n 的階乘
* @param n
* @return
*/
public long test(long n){
if(n == 1){
return 1;
}else{
return n * test(n-1);
}
}

/**
* 優化計算 n 的階乘,尾遞歸
* @param n
* @param result=1
* @return
*/
public long newTest(long n,long result){
if(n == 1){
return result;
}else{
return newTest(n - 1,n * result);
}
}

分析:上述代碼就是遞歸,通俗的講就是自己調用自己;
在執行函數test時,他也調用了另外一個函數,只不過這個函數的代碼和上一個函數的代碼一模一樣!是不是很簡單 
看一下機器層面的執行過程:
此時就需要引入棧幀的概念了:
1:棧幀將棧分割成N個記錄塊,每一個記錄塊的大小是不一樣的;
2:這個記錄塊實際上是編譯器用來實現函數調用的數據結構,通俗來講就是用於活動記錄,他用於記錄每次函數調用所涉及的相關信息的記錄單元;
3:棧幀也是一個函數的執行環境,它包括函數的參數,函數的局部變量函數,執行完之后要返回到哪里等等;

說到這里貌似,大約,好像明白了棧幀原來是用於調用函數的,你每調用一次函數他就會形成一個棧幀用於這個被調用函數的運行環境;
說到這,貌似懂了:上邊的test函數在運行時就是形成了一個又一個棧幀啊!

 

針對上邊的遞歸函數,我畫了一幅函數在棧中的執行示意圖;
棧是一種先進后出的數據結構!!!
分析:
要求計算5的階乘;
1):調用test函數時傳入5,即首先在棧中划出一個記錄塊做為函數test(5)的執行環境;執行到最后結果為: 5 * test(4);
2):上一個函數的返回值中調用函數test(4),因此繼續指向新的記錄塊,用於執行函數test(4);執行到最后結果為: 4 * test(3);
3):上一個函數的返回值中調用函數test(3),因此繼續指向新的記錄塊,用於執行函數test(3);執行到最后結果為: 3 * test(2);
4):上一個函數的返回值中調用函數test(2),因此繼續指向新的記錄塊,用於執行函數test(2);執行到最后結果為: 2 * test(1);
5):上一個函數的返回值中調用函數test(1),因此繼續指向新的記錄塊,用於執行函數test(1);執行到最后test(1)=1;
此時進棧操作已經到達了遞歸終止的條件,為了計算出最后的test(5)的值需要執行出棧操作;

 

如上圖,我畫了一幅出棧示意圖;棧是先進后出的,所以最后進的要先出。
1):test(1)出棧,返回值為1;
2):棧幀test(2)接收test(1)返回值進行計算得出test(2) = 2 * 1 = 2;
3):test(2)出棧,棧幀test(3)接收test(2)返回值進行計算得出test(3) = 3 * 2 = 6;
4):test(3)出棧,棧幀test(4)接收test(3)返回值進行計算得出test(4) = 4 * 6 = 24;
5):test(4)出棧,棧幀test(5)接收test(4)返回值進行計算得出test(5) = 5 * 24 = 120;
6):test(5)出棧,返回值120,此時表示這一段程序已經執行完畢,計算得出5的階乘是120;
遞歸函數寫到這一步,貌似是已經完美了,但是你有沒有想過:每一個函數test(n) = n * test (n-1)因此每一個棧幀不僅需要保存n值還要記錄下一個棧幀的返回值,然后才能計算出來當前棧幀的結果,因此使用多個棧幀是不可避免的,計算5的階乘就使用了5個棧幀,那要是計算100的呢?10000的呢?。。。。這TM是不是有點始料未及了?棧的大小也是有限的,你就這么用下去,他不給你溢出才怪。

上面的方法實際執行情況

test(10) // 89
test(100) // 堆棧溢出
test()方法100就計算不出來了
newTest(100,1) // 573147844013817200000
newTest(1000,1) // 7.0330367711422765e+208
newTest(10000,1) // Infinity

尾遞歸優化只在嚴格模式下生效,那么正常模式下,或者那些不支持該功能的環境中,有沒有辦法也使用尾遞歸優化呢?回答是可以的,就是自己實現尾遞歸優化。

它的原理非常簡單。尾遞歸之所以需要優化,原因是調用棧太多,造成溢出,那么只要減少調用棧,就不會溢出。怎么做可以減少調用棧呢?就是采用“循環”換掉“遞歸”。

    /**
     * 循環
     * @param n
     * @param result=1
     * @return
     */
    public long doWhell(long n,long result){
        while (n>1){
            result=result*n;
            n--;
        }
        return  result;
    }

綜上,遞歸算法涉及到棧,而棧又是先進后出的原則,因此大量的遞歸很容易造成棧的內存溢出,因此需要優化。

方向是:遞歸>>>尾遞歸>>>循環

所以你不懂遞歸 還是別玩

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM