遞歸,尾遞歸,回溯
阿新 • • 發佈:2018-03-25
total 方法調用 返回 系列 其實在 dfs 容易 遞歸 做什麽
一、首先我們講講遞歸
- 遞歸的本質是,某個方法中調用了自身。本質還是調用一個方法,只是這個方法正好是自身而已
- 遞歸因為是在自身中調用自身,所以會帶來以下三個顯著特點:
- 調用的是同一個方法
- 因為1,所以只需要寫一個方法,就可以讓你輕松調用無數次(不用一個個寫,你定個n就能有n個方法),所以調用的方法數可能非常巨大
- 在自身中調用自身,是嵌套調用(棧幀無法回收,開銷巨大)
- 因為上面2和3兩個特點,所以遞歸調用最大的詬病就是開銷巨大,棧幀和堆一起爆掉,俗稱內存泄露
- 一個誤區,不是因為調用自身而開銷巨大,而是嵌套加上輕易就能無數次調用,使得遞歸可以很容易開銷巨大
- 尾遞歸優化是利用上面的第一個特點“調用同一個方法”來進行優化的
- 尾遞歸優化其實包括兩個東西:1)尾遞歸的形式;2)編譯器對尾遞歸的優化
- 尾遞歸的形式
- 尾遞歸其實只是一種對遞歸的特殊寫法,這種寫法原本並不會帶來跟遞歸不一樣的影響,它只是寫法不一樣而已,寫成這樣不會有任何優化效果,該爆的棧和幀都還會爆
- 具體不一樣在哪裏
- 前面說了,遞歸的本質是某個方法調用了自身,尾遞歸這種形式就要求:某個方法調用自身這件事,一定是該方法做的最後一件事(所以當有需要返回值的時候會是return f(n),沒有返回的話就直接是f(n)了)
- 要求很簡單,就一條,但是有一些常見的誤區
- 這個f(n)外不能加其他東西,因為這就不是最後一件事了,值返回來後還要再幹點其他的活,變量空間還需要保留
- 比如如果有返回值的,你不能:乘個常數 return 3f(n);乘個n return n*f(n);甚至是 f(n)+f(n-1)
- 另外,使用return的尾遞歸還跟函數式編程有一點關系
- 編譯器對尾遞歸的優化
- 上面說了,你光手動寫成尾遞歸的形式,並沒有什麽卵用,要實現優化,還需要編譯器中加入了對尾遞歸優化的機制
- 有了這個機制,編譯的時候,就會自動利用上面的特點一來進行優化
- 具體是怎麽優化的:
- 簡單說就是重復利用同一個棧幀,不僅不用釋放上一個,連下一個新的都不用開,效率非常高(有人做實驗,這個比遞推比叠代都要效率高)
- 為什麽寫成尾遞歸的形式,編譯器就能優化了?或者說【編譯器對尾遞歸的優化】的一些深層思想
- 說是深層思想,其實也是因為正好編譯器其實在這裏沒做什麽復雜的事,所以很簡單
- 由於這兩方面的原因,尾遞歸優化得以實現,而且效果很好
- 因為在遞歸調用自身的時候,這一層函數已經沒有要做的事情了,雖然被遞歸調用的函數是在當前的函數裏,但是他們之間的關系已經在傳參的時候了斷了,也就是這一層函數的所有變量什麽的都不會再被用到了,所以當前函數雖然沒有執行完,不能彈出棧,但它確實已經可以出棧了,這是一方面
- 另一方面,正因為調用的是自身,所以需要的存儲空間是一毛一樣的,那幹脆重新刷新這些空間給下一層利用就好了,不用銷毀再另開空間
- 有人對寫成尾遞歸形式的說法是【為了告訴編譯器這塊要尾遞歸】,這種說法可能會導致誤解,因為不是只告訴編譯器就行,而是你需要做優化的前半部分,之後編譯器做後半部分
- 所以總結:為了解決遞歸的開銷大問題,使用尾遞歸優化,具體分兩步:1)你把遞歸調用的形式寫成尾遞歸的形式;2)編譯器碰到尾遞歸,自動按照某種特定的方式進行優化編譯
回溯就是讓計算機自動的去搜索,碰到符合的情況就結束或者保存起來,在一條路徑上走到盡頭也不能找出解,就回到原來的岔路口,選擇一條以前沒有走過的路繼續探測,直到找到解或者走完所有路徑為止。就這一點,回溯和所謂的DFS(深度優先搜索)是一樣的。那現在的關鍵是,怎麽實現搜索呢?回溯既然一般使用遞歸來實現,那個這個遞歸調用該如何來寫呢?我現在的理解就是,進行回溯搜索都會有一系列的步驟,每一步都會進行一些查找。而每一步的情況除了輸入會不一樣之外,其他的情況都是一致的。這就剛好滿足了遞歸調用的需求。通過把遞歸結束的條件設置到搜索的最後一步,就可以借用遞歸的特性來回溯了。因為合法的遞歸調用總是要回到它的上一層調用的,那麽在回溯搜索中,回到上一層調用就是回到了前一個步驟。當在當前步驟找不到一種符合條件情況時,那麽後續情況也就不用考慮了,所以就讓遞歸調用返回上一層(也就是回到前一個步驟)找一個以前沒有嘗試過的情況繼續進行。當然有時候為了能夠正常的進行繼續搜索,需要恢復以前的調用環境。
遞歸,尾遞歸,回溯