在最近作項目的時候碰到某個統計查詢比較慢,比較影響效率,通過排查發現是這個程序使用了遞歸調用,我遍歷的是組織架構,數據庫
當這個層級很深時這個程序就會越慢,架構
這是我寫的統計某個組織的下屬組織總數函數
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)); 這樣則只保留後一個函數堆棧便可
以上我寫的優化查詢的確實有問題,但遞歸與尾遞歸的解釋仍是能夠參考的