<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          都說不建議使用遞歸操作,到底為什么?

          共 3626字,需瀏覽 8分鐘

           ·

          2022-05-31 05:26

          點擊關(guān)注公眾號,Java干貨及時送達

          作者:CG國斌

          來源:blog.csdn.net/qq_35246620/article/details/104855300


          遞歸的問題

          如題,我們可能或多或少的都聽見過類似的話或者建議:

          盡量少使用遞歸操作,甚至干脆就不要使用遞歸操作。

          但我們在聽到這句話的時候,是否會產(chǎn)生過疑問,為什么不建議使用遞歸操作呢?

          現(xiàn)在,我們就一起聊聊這個話題,看看遞歸到底會產(chǎn)生什么樣的問題。

          首先,我們思考一道算法題:如何實現(xiàn)二叉樹的中序遍歷?

          對于樹的遍歷,無論是前序、中序還是后序遍歷,大家可能下意識的就會想到遞歸,為什么呢?因為遞歸操作實現(xiàn)起來“簡單”啊,而且樹的結(jié)構(gòu)完美契合了遞歸的應(yīng)用場景!下面為實現(xiàn)二叉樹中序遍歷的遞歸實現(xiàn):

          ????public?List?inorder(TreeNode?root)?{
          ????????List?ans?=?new?ArrayList<>();
          ????????helper(root,?ans);
          ????????return?ans;
          ????}

          ????private?void?helper(TreeNode?root,?List?ans)?{
          ????????if?(root?!=?null)?{
          ????????????if?(root.left?!=?null)?{
          ????????????????helper(root.left,?ans);
          ????????????}
          ????????????ans.add(root.val);
          ????????????if?(root.right?!=?null)?{
          ????????????????helper(root.right,?ans);
          ????????????}
          ????????}
          ????}

          觀察上述代碼,在使用遞歸的時候,我們會在函數(shù)的某一部分,重復(fù)的調(diào)用某個函數(shù)自身,直到觸發(fā)終止條件時,遞歸才會停止,進而函數(shù)才會執(zhí)行完畢。說到這里,我們就發(fā)現(xiàn)了遞歸可能會產(chǎn)生問題的第一個地方:

          • 如果終止條件有問題,那么遞歸將無法停止。

          那么,我們進一步分析,如果遞歸無法停止,又會出現(xiàn)什么問題呢?

          • 如果遞歸無法停止,函數(shù)會不斷的調(diào)用自身,從而無法執(zhí)行后序的流程。

          其表現(xiàn)出來的現(xiàn)象,就是程序卡在了某處,無法繼續(xù)執(zhí)行。到這里,我們已經(jīng)從邏輯上分析了遞歸可能會產(chǎn)生的問題。接下來,我們再從 JVM 的層面上,分析遞歸可能會產(chǎn)生的問題。

          我們知道,Java 源代碼需要編譯成字節(jié)碼文件,然后由 JVM 解釋執(zhí)行,為了能高效地管理程序方法的調(diào)用,有條不紊地進行嵌套的方法調(diào)用和方法返回,JVM 維護了一個棧結(jié)構(gòu),稱為虛擬機方法棧(如果調(diào)用的是 Native 方法,則為本地方法棧)。

          棧里面存放的一個個實體稱為棧幀,每個棧幀都包括了局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址和一些額外的附加信息。在 JVM 中,方法調(diào)用的過程大致為:

          • 除非被調(diào)用的方法是類方法,否則在每一次方法調(diào)用指令之前,JVM 會先把方法被調(diào)用的對象引用壓入操作數(shù)棧中,除了對象的引用之外,JVM 還會把方法的參數(shù)依次壓入操作數(shù)棧;
          • 在執(zhí)行方法調(diào)用指令時,JVM 會將函數(shù)參數(shù)和對象引用依次從操作數(shù)棧彈出,并新建一個棧幀,把對象引用和函數(shù)參數(shù)分別放入新棧幀的局部變量表;
          • JVM 把新棧幀壓入虛擬機方法棧,并把 PC(程序計數(shù)器)指向函數(shù)的第一條待執(zhí)行的指令。

          因此,我們總是說,每個方法的執(zhí)行過程,都是一個棧幀從入棧到出棧的過程。這意味著,在執(zhí)行遞歸操作的時候,如果終止條件有問題,無法終止遞歸,則會出現(xiàn):

          • 虛擬機方法棧只入棧不出棧

          進而,當(dāng)棧中所有棧幀的大小總和大于-Xss設(shè)置的值時,就會出現(xiàn)棧溢出或者稱之為棧擊穿,即:

          • 拋出StackOverflowError異常

          此外,函數(shù)的執(zhí)行是有一定開銷的,例如每次都要保存局部變量、參數(shù)、調(diào)用函數(shù)地址、返回值等,而遞歸的開銷還要在此基礎(chǔ)上乘以迭代次數(shù),這自然會影響到函數(shù)的執(zhí)行效率。

          但對于某些問題,如上面我們考慮的二叉樹的中序遍歷,在條件允許的情況下,我們還是傾向于使用遞歸實現(xiàn)的,因為相對來說,遞歸的實現(xiàn)更簡單,也更容易理解。

          優(yōu)化的方法

          說的這里,我們不妨再來聊聊如何優(yōu)化遞歸,其方法主要有三個,分別為:

          • 限制遞歸次數(shù)
          • 借助堆棧將遞歸轉(zhuǎn)化為非遞歸
          • 使用尾遞歸形式

          限制遞歸次數(shù)

          對于“限制遞歸次數(shù)”來說,就是在調(diào)用函數(shù)的時候,同時傳入一個數(shù)字 N 作為遞歸的次數(shù),當(dāng)遞歸次數(shù)大于 N 的時候,強制結(jié)束遞歸并返回結(jié)果。仍以實現(xiàn)二叉樹的中序遍歷為例,在上述的遞歸實現(xiàn)之上,我們新增了一個int類型的參數(shù)level,作為遞歸可執(zhí)行的最大次數(shù),代碼示例為:

          ????public?List?inorder(TreeNode?root,?int?level)?{
          ????????List?ans?=?new?ArrayList<>();
          ????????helper(root,?ans,?level);
          ????????return?ans;
          ????}

          ????private?void?helper(TreeNode?root,?List?ans,?int?level)?{
          ????????if?(level?>=?0)?{
          ????????????if?(root?!=?null)?{
          ????????????????if?(root.left?!=?null)?{
          ????????????????????helper(root.left,?ans,?level?-?1);
          ????????????????}
          ????????????????ans.add(root.val);
          ????????????????if?(root.right?!=?null)?{
          ????????????????????helper(root.right,?ans,?level?-?1);
          ????????????????}
          ????????????}
          ????????}
          ????}

          如上述代碼所示,限制迭代次數(shù)能夠有效的防止棧溢出或者說是棧擊穿的問題,但卻有可能得不到我們想要的“正確”的結(jié)果。

          例如,一棵 10 層的二叉樹,我們調(diào)用上述的inorder方法,將level設(shè)置為 5,即使用inorder(root, 5)來進行遍歷,這意味著我們僅能遍歷出這棵 10 層樹的前 5 層,并沒有把這棵樹完全遍歷出來,因此限制遞歸次數(shù)的方法是有瑕疵的,治標不治本。

          借助堆棧將遞歸轉(zhuǎn)化為非遞歸

          對于“借助堆棧將遞歸轉(zhuǎn)化為非遞歸”來說,就是利用堆棧模擬遞歸的執(zhí)行過程,這種方法幾乎是通用的方法,因為遞歸本身就是通過堆棧實現(xiàn)的,我們只要把遞歸函數(shù)調(diào)用的局部變量和相應(yīng)的狀態(tài)放入到一個棧結(jié)構(gòu)中,在函數(shù)調(diào)用和返回時做好push和pop操作,就可以了。仍以實現(xiàn)二叉樹的中序遍歷為例,我們利用堆棧將其改造為非遞歸的形式:

          ????public?List?inorder(TreeNode?root)?{
          ????????List?ans?=?new?ArrayList<>();
          ????????Stack?stack?=?new?Stack<>();
          ????????TreeNode?curr?=?root;
          ????????while?(curr?!=?null?||?!stack.isEmpty())?{
          ????????????while?(curr?!=?null)?{
          ????????????????stack.push(curr);
          ????????????????curr?=?curr.left;
          ????????????}
          ????????????curr?=?stack.pop();
          ????????????ans.add(curr.val);
          ????????????curr?=?curr.right;
          ????????}
          ????????return?ans;
          ????}

          如上述代碼所示,我們利用Stack來存儲二叉樹的節(jié)點,由于中序遍歷的順序為首先遍歷左子樹、然后訪問根結(jié)點、最后遍歷右子樹,因此我們從根節(jié)點開始,依次將左節(jié)點壓入棧,直至把左子樹遍歷完,然后再依次彈棧,并將彈出的節(jié)點值存入我們設(shè)置的結(jié)果列表ans,最后再將當(dāng)前節(jié)點的右節(jié)點賦值給當(dāng)前節(jié)點,以保證后續(xù)的遍歷,如此循環(huán)即可。

          使用尾遞歸形式

          對于“使用尾遞歸形式”來說,則是將遞歸中對函數(shù)本身的調(diào)用下移到函數(shù)的最后一行。因此,像我們上面實現(xiàn)的二叉樹的中序遍歷,就很難用尾遞歸的形式來改寫,因為遞歸形式的中序遍歷需要在遍歷左右子樹之間,把結(jié)果存起來,從而給在函數(shù)最后一行調(diào)用函數(shù)自身的形式造成了很大的困難。

          在此,我們以實現(xiàn)斐波那契數(shù)列為例,演示普通的遞歸形式與尾遞歸形式的區(qū)別:

          • 普通遞歸形式
          public?int?fibonacci(int?n)?{
          ????if?(n?2)?{
          ????????return?n;
          ????}
          ????return?fibonacci(n?-?1)?+?fibonacci(n?-?2);
          }
          • 尾遞歸形式
          public?int?fibonacciTail(int?n,?int?fn1,?int?fn2)?{
          ????if?(n?==?0)?{
          ????????return?fn1;
          ????}
          ????return?fibonacciTail(n?-?1,?fn2,?fn1?+?fn2);
          }

          我們之所以說尾遞歸是對普通遞歸形式的優(yōu)化,其原因在于:普通遞歸,每次執(zhí)行遞歸調(diào)用的時候,JVM 都需要為局部變量創(chuàng)建棧來存儲;而尾遞歸,則是因為對函數(shù)自身的調(diào)用在尾部,因此根本不需要新創(chuàng)建棧來保持任何局部變量,直接傳遞參數(shù)即可,減少了?N - 1?個新棧的創(chuàng)建,其中 N 為需要遞歸的次數(shù)。

          說了這么多,那么尾遞歸形式是否真的有優(yōu)化效果呢?我們不妨寫一個簡單的程序,來驗證一下:

          public?class?RecursiveMethodTest?{
          ????public?static?int?fibonacci(int?n)?{
          ????????if?(n?2)?{
          ????????????return?n;
          ????????}
          ????????return?fibonacci(n?-?1)?+?fibonacci(n?-?2);
          ????}

          ????public?static?int?fibonacciTail(int?n,?int?fn1,?int?fn2)?{
          ????????if?(n?==?0)?{
          ????????????return?fn1;
          ????????}
          ????????return?fibonacciTail(n?-?1,?fn2,?fn1?+?fn2);
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????int?n?=?30;
          ????????long?d1?=?System.nanoTime();
          ????????System.out.println("普通遞歸結(jié)果:"?+?fibonacci(n));
          ????????long?d2?=?System.nanoTime();
          ????????System.out.println("普通遞歸形式:遞歸?"?+?n?+?"?次,耗時?"?+?(d2?-?d1)?+?"?納秒。");
          ????????long?d3?=?System.nanoTime();
          ????????System.out.println("尾遞歸結(jié)果:"?+?fibonacciTail(n,?0,?1));
          ????????long?d4?=?System.nanoTime();
          ????????System.out.println("尾遞歸形式:遞歸?"?+?n?+?"?次,耗時?"?+?(d4?-?d3)?+?"?納秒。");
          ????}
          }

          其執(zhí)行結(jié)果為:

          普通遞歸結(jié)果:832040
          普通遞歸形式:遞歸 30?次,耗時 4896196 納秒。
          尾遞歸結(jié)果:832040
          尾遞歸形式:遞歸 30?次,耗時 38125 納秒。

          如上述結(jié)果所示,尾遞歸與普通遞歸相比,快了近 128 倍。雖然這樣的測試還很粗糙,但也足以說明兩者的性能差異啦!

          ? ? ?

          1、我在產(chǎn)品上線前不小心刪除了7 TB的視頻
          2、程序員最硬大佬,你絕對想不到?。。?/span>
          3、IntelliJ IDEA快捷鍵大全 + 動圖演示
          4、打不過就加入?微軟強推“親兒子”上位,還是中國特供版
          5、活久見!NVIDIA正式開源其Linux GPU內(nèi)核模塊

          點分享

          點收藏

          點點贊

          點在看

          瀏覽 16
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品 A片在线 | 操逼网站欧美 | 高清无码黄色电影 | 成人黄色视频网站在线 | 亚洲最大中文字幕在线 |