遞歸與尾遞歸

在最近作項目的時候碰到某個統計查詢比較慢,比較影響效率,通過排查發現是這個程序使用了遞歸調用,我遍歷的是組織架構,數據庫

當這個層級很深時這個程序就會越慢,架構

這是我寫的統計某個組織的下屬組織總數函數

 1    /**
 2      * 疊加父級下子級的數量(遞歸調用)
 3      * @param pd 封裝的PageData用來接受參數(能夠理解成Map)
 4      * @param res  初始爲0
 5      * @return
 6      * @throws Exception
 7      */
 8     public Integer listsubType(PageData pd,int res) throws Exception{
 9         int count=this.subType(pd);//統計該組織數量(能夠是人員或者類型數量)
10         res+=count;//疊加每次的數量
11         List<PageData> list=listParentId(pd);//遍歷該組織下屬組織架構
12         for(PageData pd2:list){//遍歷若是無則跳出
13            this.listsubType(pd2,res);//繼續調用
14         }
15         return res;//返回統計結果
16     }

這個是我本地的數據庫,數據量並很少測試

普通遞歸執行花費時間優化

怎麼優化一開始我並無思路,可是經過百度我逐漸瞭解遞歸的效率可謂是聲名狼藉,那麼效率與代碼簡潔是否能夠共存呢?答案是確定的this

尾遞歸  百度百科的解釋爲:若是一個函數中全部遞歸形式的調用都出如今函數的末尾,咱們稱這個遞歸函數是尾遞歸的spa

讓函數在末尾被調用,好辦, 因而稍微改了下代碼(其實就是把this改成了return)
code

 1 /**
 2      * 疊加父級下子級的數量(遞歸調用)
 3      * @param pd 封裝的PageData用來接受參數(能夠理解成Map)
 4      * @param res  初始爲0
 5      * @return
 6      * @throws Exception
 7      */
 8     public Integer listsubType(PageData pd,int res) throws Exception{
 9         int count=this.subType(pd);//統計數量
10         res+=count;//疊加每次的數量
11         List<PageData> list=listParentId(pd);//遍歷該組織下屬組織架構
12         for(PageData pd2:list){//遍歷若是無則跳出
13            return listsubType(pd2,res);//繼續調用
14         }
15         return res;//返回統計結果
16     }

尾遞歸執行花費時間blog

 

差距是否是很明顯,固然我取得只是測試中某一個值,二者之間花費時間我也各測試了好幾回,我取的是大概中間值遞歸

其實到了這一步也差很少完成了優化,可我對這其中的原理仍是隻知其一;不知其二,普通遞歸好理解,但尾遞歸呢?因而繼續研究

 1 // 例一
 2 function f(x){
 3   let y = f(x);
 4   return y;
 5 }
 6 
 7 // 例二
 8 function f(x){
 9   return f(x) + 1;
10 }

上面的的兩個都不屬於尾遞歸(不對,應該是不屬於尾調用),由於在調用自身後它們還須要進行操做,

尾調用:尾調用是指一個函數裏的最後一個動做是一個函數調用的情形,即這個調用的返回值直接被當前函數返回的情形。這種情形下稱該調用位置稱爲「尾位置」

尾遞歸:若一個函數在尾位置調用自身,則稱這種狀況爲尾遞歸。尾遞歸是遞歸的一種特殊情形

這樣也算是初步理解了尾遞歸與尾調用的概念了

那麼爲何普通遞歸與尾遞歸的查詢效率會差這麼多呢

查詢資料得:

函數調用會在內存造成一個"調用記錄",又稱"調用幀"(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用記錄上方,還會造成一個B的調用記錄。等到B運行結束,將結果返回到A,B的調用記錄纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用記錄棧,以此類推。全部的調用記錄,就造成一個"調用棧"(call stack)

尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用記錄,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用記錄,取代外層函數的調用記錄就能夠了

本身理解的以下

這是用普通遞歸求1到n的和

 

 1 int fn( int n )
 2 {
 3     int result;
 4     if(n<=0)
 5     result=0;
 6     else if(n==1)
 7     result=1;
 8     else
 9     result=fn(n-1)+n;
10     return result;
11 }

 

若是輸入的是3,那麼你們理解的就是2+3=5,裏面的真實計算爲fn(0)+fn(1)+fn(2)+fn(3)

這是用尾遞歸求1到n的和

1 int fn( int n,int res)
2     {
3         if(n<=0)
4            return res=0;
5         else if(n==1)
6            return res;
7 
8         return fn(n-1,n+res);
9     }

若是輸入的是3,那麼就是2+3=5等價於fn(2)+fn(3),由於2和3已經計算好的能夠直接拿過來用,而不須要再次計算

不用尾遞歸,函數的堆棧耗用難以估量,須要保存不少中間函數的堆棧。好比f(n, sum) = f(n-1) + value(n) + sum; 會保存n個函數調用堆棧,而使用尾遞歸f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函數堆棧便可

以上我寫的優化查詢的確實有問題,但遞歸與尾遞歸的解釋仍是能夠參考的

相關文章
相關標籤/搜索