0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何將前中后序的遞歸框架改寫成迭代形式

算法與數(shù)據(jù)結(jié)構(gòu) ? 來源:labuladong ? 作者:labuladong ? 2022-03-18 10:13 ? 次閱讀

之前經(jīng)常講涉及遞歸的算法題,我說過寫遞歸算法的一個技巧就是不要試圖跳進遞歸細節(jié),而是從遞歸框架上思考,從函數(shù)定義去理解遞歸函數(shù)到底該怎么實現(xiàn)。

而且我們前文學習數(shù)據(jù)結(jié)構(gòu)和算法的框架思維特別強調(diào)過,練習遞歸的框架思維最好方式就是從二叉樹相關(guān)題目開始刷,前文也有好幾篇手把手帶你刷二叉樹和二叉搜索樹的文章:

之前的文章全部都是運用二叉樹的遞歸框架,幫你透過現(xiàn)象看本質(zhì),明白二叉樹的各種題目本質(zhì)都是前中后序遍歷衍生出來的。

前文BFS 算法框架詳解是利用隊列迭代地遍歷二叉樹,不過使用的是層級遍歷,沒有遞歸遍歷中的前中后序之分。

由于現(xiàn)在面試越來越卷,很多讀者在后臺問我如何將前中后序的遞歸框架改寫成迭代形式。

首先我想說,遞歸改迭代從實用性的角度講是沒什么意義的,明明可以寫遞歸解法,為什么非要改成迭代的方式?

對于二叉樹來說,遞歸解法是最容易理解的,非要讓你改成迭代,頂多是考察你對遞歸和棧的理解程度,架不住大家問,那就總結(jié)一下吧。

我以前見過一些迭代實現(xiàn)二叉樹前中后序遍歷的代碼模板,比較短小,容易記,但通用性較差。

通用性較差的意思是說,模板只是針對「用迭代的方式返回二叉樹前/中/后序的遍歷結(jié)果」這個問題,函數(shù)簽名類似這樣,返回一個TreeNode列表:

Listtraverse(TreeNoderoot);

如果給一些稍微復雜的二叉樹問題,比如最近公共祖先,二叉搜索子樹的最大鍵值和,想把這些遞歸解法改成迭代,就無能為力了。

而我想要的是一個萬能的模板,可以把一切二叉樹遞歸算法都改成迭代。

換句話說,類似二叉樹的遞歸框架:

voidtraverse(TreeNoderoot){
if(root==null)return;
/*前序遍歷代碼位置*/
traverse(root.left);
/*中序遍歷代碼位置*/
traverse(root.right);
/*后序遍歷代碼位置*/
}

迭代框架也應該有前中后序代碼的位置:

voidtraverse(TreeNoderoot){
while(...){
if(...){
/*前序遍歷代碼位置*/
}
if(...){
/*中序遍歷代碼位置*/
}
if(...){
/*后序遍歷代碼位置*/
}
}
}

我如果想把任何一道二叉樹遞歸解法改成迭代,直接把遞歸解法中前中后序?qū)恢玫拇a復制粘貼到迭代框架里,就可以直接運行,得到正確的結(jié)果。

理論上,所有遞歸算法都可以利用棧改成迭代的形式,因為計算機本質(zhì)上就是借助棧來迭代地執(zhí)行遞歸函數(shù)的。

所以本文就來利用「?!?a href="http://www.ttokpm.com/analog/" target="_blank">模擬函數(shù)遞歸的過程,總結(jié)一套二叉樹通用迭代遍歷框架。

遞歸框架改為迭代

參加過我的二叉樹專項訓練的讀者應該知道,二叉樹的遞歸框架中,前中后序遍歷位置就是幾個特殊的時間點:

前序遍歷位置的代碼,會在剛遍歷到當前節(jié)點root,遍歷root的左右子樹之前執(zhí)行;

中序遍歷位置的代碼,會在在遍歷完當前節(jié)點root的左子樹,即將開始遍歷root的右子樹的時候執(zhí)行;

后序遍歷位置的代碼,會在遍歷完以當前節(jié)點root為根的整棵子樹之后執(zhí)行。

0320d1fc-93aa-11ec-952b-dac502259ad0.jpg

如果從遞歸代碼上來看,上述結(jié)論是很容易理解的:

voidtraverse(TreeNoderoot){
if(root==null)return;
/*前序遍歷代碼位置*/
traverse(root.left);
/*中序遍歷代碼位置*/
traverse(root.right);
/*后序遍歷代碼位置*/
}

不過,如果我們想將遞歸算法改為迭代算法,就不能從框架上理解算法的邏輯,而要深入細節(jié),思考計算機是如何進行遞歸的。

假設(shè)計算機運行函數(shù)A,就會把A放到調(diào)用棧里面,如果A又調(diào)用了函數(shù)B,則把B壓在A上面,如果B又調(diào)用了C,那就再把C壓到B上面……

C執(zhí)行結(jié)束后,C出棧,返回值傳給BB執(zhí)行完后出棧,返回值傳給A,最后等A執(zhí)行完,返回結(jié)果并出棧,此時調(diào)用棧為空,整個函數(shù)調(diào)用鏈結(jié)束。

我們遞歸遍歷二叉樹的函數(shù)也是一樣的,當函數(shù)被調(diào)用時,被壓入調(diào)用棧,當函數(shù)結(jié)束時,從調(diào)用棧中彈出。

那么我們可以寫出下面這段代碼模擬遞歸調(diào)用的過程:

//模擬系統(tǒng)的函數(shù)調(diào)用棧
Stackstk=newStack<>();

voidtraverse(TreeNoderoot){
if(root==null)return;
//函數(shù)開始時壓入調(diào)用棧
stk.push(root);
traverse(root.left);
traverse(root.right);
//函數(shù)結(jié)束時離開調(diào)用棧
stk.pop();
}

如果在前序遍歷的位置入棧,后序遍歷的位置出棧,stk中的節(jié)點變化情況就反映了traverse函數(shù)的遞歸過程(綠色節(jié)點就是被壓入棧中的節(jié)點,灰色節(jié)點就是彈出棧的節(jié)點):

03342018-93aa-11ec-952b-dac502259ad0.gif

簡單說就是這樣一個流程:

1、拿到一個節(jié)點,就一路向左遍歷(因為traverse(root.left)排在前面),把路上的節(jié)點都壓到棧里。

2、往左走到頭之后就開始退棧,看看棧頂節(jié)點的右指針,非空的話就重復第 1 步。

寫成迭代代碼就是這樣:

privateStackstk=newStack<>();

publicListtraverse(TreeNoderoot){
pushLeftBranch(root);

while(!stk.isEmpty()){
TreeNodep=stk.pop();
pushLeftBranch(p.right);
}
}

//左側(cè)樹枝一擼到底,都放入棧中
privatevoidpushLeftBranch(TreeNodep){
while(p!=null){
stk.push(p);
p=p.left;
}
}

上述代碼雖然已經(jīng)可以模擬出遞歸函數(shù)的運行過程,不過還沒有找到遞歸代碼中的前中后序代碼位置,所以需要進一步修改。

迭代代碼框架

想在迭代代碼中體現(xiàn)前中后序遍歷,關(guān)鍵點在哪里?

當我從棧中拿出一個節(jié)點p,我應該想辦法搞清楚這個節(jié)點p左右子樹的遍歷情況

如果p的左右子樹都沒有被遍歷,那么現(xiàn)在對p進行操作就屬于前序遍歷代碼。

如果p的左子樹被遍歷過了,而右子樹沒有被遍歷過,那么現(xiàn)在對p進行操作就屬于中序遍歷代碼。

如果p的左右子樹都被遍歷過了,那么現(xiàn)在對p進行操作就屬于后序遍歷代碼。

上述邏輯寫成偽碼如下:

privateStackstk=newStack<>();

publicListtraverse(TreeNoderoot){
pushLeftBranch(root);

while(!stk.isEmpty()){
TreeNodep=stk.peek();

if(p的左子樹被遍歷完了){
/*******************/
/**中序遍歷代碼位置**/
/*******************/
//去遍歷p的右子樹
pushLeftBranch(p.right);
}

if(p的右子樹被遍歷完了){
/*******************/
/**后序遍歷代碼位置**/
/*******************/
//以p為根的樹遍歷完了,出棧
stk.pop();
}
}
}

privatevoidpushLeftBranch(TreeNodep){
while(p!=null){
/*******************/
/**前序遍歷代碼位置**/
/*******************/
stk.push(p);
p=p.left;
}
}

有剛才的鋪墊,這段代碼應該是不難理解的,關(guān)鍵是如何判斷p的左右子樹到底被遍歷過沒有呢?

其實很簡單,我們只需要維護一個visited指針,指向「上一次遍歷完成的根節(jié)點」,就可以判斷p的左右子樹遍歷情況了

下面是迭代遍歷二叉樹的完整代碼框架

//模擬函數(shù)調(diào)用棧
privateStackstk=newStack<>();

//左側(cè)樹枝一擼到底
privatevoidpushLeftBranch(TreeNodep){
while(p!=null){
/*******************/
/**前序遍歷代碼位置**/
/*******************/
stk.push(p);
p=p.left;
}
}

publicListtraverse(TreeNoderoot){
//指向上一次遍歷完的子樹根節(jié)點
TreeNodevisited=newTreeNode(-1);
//開始遍歷整棵樹
pushLeftBranch(root);

while(!stk.isEmpty()){
TreeNodep=stk.peek();

//p的左子樹被遍歷完了,且右子樹沒有被遍歷過
if((p.left==null||p.left==visited)
&&p.right!=visited){
/*******************/
/**中序遍歷代碼位置**/
/*******************/
//去遍歷p的右子樹
pushLeftBranch(p.right);
}
//p的右子樹被遍歷完了
if(p.right==null||p.right==visited){
/*******************/
/**后序遍歷代碼位置**/
/*******************/
//以p為根的子樹被遍歷完了,出棧
//visited指針指向p
visited=stk.pop();
}
}
}

代碼中最有技巧性的是這個visited指針,它記錄最近一次遍歷完的子樹根節(jié)點(最近一次pop出棧的節(jié)點),我們可以根據(jù)對比p的左右指針和visited是否相同來判斷節(jié)點p的左右子樹是否被遍歷過,進而分離出前中后序的代碼位置。

PS:visited指針初始化指向一個新 new 出來的二叉樹節(jié)點,相當于一個特殊值,目的是避免和輸入二叉樹中的節(jié)點重復。

只需把遞歸算法中的前中后序位置的代碼復制粘貼到上述框架的對應位置,就可以把任意遞歸的二叉樹算法改寫成迭代形式了。

比如,讓你返回二叉樹后序遍歷的結(jié)果,你就可以這樣寫:

privateStackstk=newStack<>();

publicListpostorderTraversal(TreeNoderoot){
//記錄后序遍歷的結(jié)果
Listpostorder=newArrayList<>();
TreeNodevisited=newTreeNode(-1);

pushLeftBranch(root);
while(!stk.isEmpty()){
TreeNodep=stk.peek();

if((p.left==null||p.left==visited)
&&p.right!=visited){
pushLeftBranch(p.right);
}

if(p.right==null||p.right==visited){
//后序遍歷代碼位置
postorder.add(p.val);
visited=stk.pop();
}
}

returnpostorder;
}

privatevoidpushLeftBranch(TreeNodep){
while(p!=null){
stk.push(p);
p=p.left;
}
}

當然,任何一個二叉樹的算法,如果你想把遞歸改成迭代,都可以套用這個框架,只要把遞歸的前中后序位置的代碼對應過來就行了。

迭代解法到這里就搞定了,不過我還是想強調(diào),除了 BFS 層級遍歷之外,二叉樹的題目還是用遞歸的方式來做,因為遞歸是最符合二叉樹結(jié)構(gòu)特點的。

說到底,這個迭代解法就是在用棧模擬遞歸調(diào)用,所以對照著遞歸解法,應該不難理解和記憶。

原文標題:二叉樹八股文:遞歸改迭代通用模板

文章出處:【微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 算法
    +關(guān)注

    關(guān)注

    23

    文章

    4552

    瀏覽量

    92023
  • 模板
    +關(guān)注

    關(guān)注

    0

    文章

    107

    瀏覽量

    20531
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4237

    瀏覽量

    61967

原文標題:二叉樹八股文:遞歸改迭代通用模板

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    labview遞歸使用你嘗試過嗎?

    遞歸VI factorial.vi 例子通過當前數(shù)與當前數(shù)-1的階乘相乘而得,也就是調(diào)用了自己。 數(shù)學公式如下,3!=3*(2!)。在這個遞歸factorial VI,1!和0!
    發(fā)表于 01-05 15:07

    用Kei寫程序的時候,怎么頭文件為AT89X51.H的程序改寫成...

    用Kei寫程序的時候,怎么頭文件為AT89X51.H的程序改寫成頭文件為REG51.H的。這兩種頭文件寫程序有什么區(qū)別?l
    發(fā)表于 11-15 21:10

    《C Primer Plus》讀書筆記——遞歸

    ("LEVEL %d: n location %p\n" , n, &n);}輸出如下:遞歸的基本原理每級遞歸都使用其私有變量(如例子的n)每次函數(shù)調(diào)用都返回一級(調(diào)用他那級
    發(fā)表于 02-05 20:06

    快速掌握Python的遞歸函數(shù)與匿名函數(shù)調(diào)用

    factorial函數(shù)調(diào)用factorial函數(shù)的情況。出現(xiàn)了函數(shù)的遞歸。為了完善上述代碼,可以代碼的第二部也翻譯成代碼:  def factorial(n):  1. int
    發(fā)表于 07-19 16:22

    LabVIEW遞歸調(diào)用

    一.NI提供的遞歸調(diào)用使用的步驟如下1.VI設(shè)置成重載模式2.使用靜態(tài)調(diào)用調(diào)用調(diào)用VI,實現(xiàn)自身調(diào)用看見下圖NI自帶遞歸方法二、如果靜態(tài)調(diào)用改成直接調(diào)用自身也可得到相同的結(jié)果,而且
    發(fā)表于 05-18 10:36

    遞歸最小二乘法

    一、遞歸最小二乘法遞推最小二乘法:當矩陣維數(shù)增加時,矩陣求逆運算計算量過大,而且不適合在線辨識。為了減少計算量,并且可以實時地辨識出動態(tài)系統(tǒng)的特性,可以最小二乘法轉(zhuǎn)換成參數(shù)遞推的估計。取N組數(shù)據(jù)
    發(fā)表于 08-27 07:03

    LabVIEW中使用遞歸算法

    ?Open VI Reference。遞歸VI路徑連上,并將options的輸入設(shè)置為8,這樣可以使通過引用調(diào)用可重入VI有效。在Open VI Reference圖標的type specifier VI
    發(fā)表于 04-17 20:11

    C++教程之函數(shù)的遞歸調(diào)用

    C++教程之函數(shù)的遞歸調(diào)用 在執(zhí)行函數(shù) f 的過程,又要調(diào)用 f 函數(shù)本身,稱為函數(shù)的遞歸調(diào)用;形式上:一個正在執(zhí)行的函數(shù)調(diào)用了自身;這種遞歸
    發(fā)表于 05-15 18:00 ?35次下載

    如何將IP模塊整合到System Generator for DSP

    了解如何將Vivado HLS設(shè)計作為IP模塊整合到System Generator for DSP。 了解如何將Vivado HLS設(shè)計保存為IP模塊,并了解如何將此IP輕松整合
    的頭像 發(fā)表于 11-20 05:55 ?3119次閱讀

    如何把C++的源程序改寫成C語言

    第一種是C++的面向?qū)ο筇卣魅サ?,先全部理解源代碼的邏輯,然后改寫;第二種是在C中保留面向?qū)ο蟮牟糠痔卣?,用結(jié)構(gòu)體實現(xiàn)類的功能。
    的頭像 發(fā)表于 05-14 10:08 ?2843次閱讀
    如何把C++的源程序<b class='flag-5'>改寫成</b>C語言

    C語言編程如何求出二叉樹后序遍歷

    題目 已知二叉樹前序為 ABDFGCEH 后序序列為 BFDGACEH ,要求輸出后序遍歷為 FGDBHECA 大體思路 又先序得出根,先序的根后為左樹一部分,我們再在序序列里找到先序的根,此處
    的頭像 發(fā)表于 08-23 11:04 ?3803次閱讀

    如何求遞歸算法的時間復雜度

    那么我通過一道簡單的面試題,模擬面試的場景,來帶大家逐步分析遞歸算法的時間復雜度,最后找出最優(yōu)解,來看看同樣是遞歸,怎么就寫成了O(n)的代碼。
    的頭像 發(fā)表于 07-13 11:30 ?2171次閱讀

    Python支持遞歸函數(shù)

    Python支持遞歸函數(shù)——即直接或間接地調(diào)用自身以進行循環(huán)的函數(shù)。遞歸是頗為高級的話題,并且它在Python相對少見。然而,它是一項應該了解的有用的技術(shù),因為它允許程序遍歷擁有任意的、不可預知的形狀的結(jié)構(gòu)。
    的頭像 發(fā)表于 02-21 14:28 ?568次閱讀

    如何將Pytorch自訓練模型變成OpenVINO IR模型形式

    本文章依次介紹如何將Pytorch自訓練模型經(jīng)過一系列變換變成OpenVINO IR模型形式,而后使用OpenVINO Python API 對IR模型進行推理,并將推理結(jié)果通過OpenCV API顯示在實時畫面上。
    的頭像 發(fā)表于 06-07 09:31 ?1653次閱讀
    <b class='flag-5'>如何將</b>Pytorch自訓練模型變成OpenVINO IR模型<b class='flag-5'>形式</b>

    遞歸神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)形式主要分為

    結(jié)構(gòu)形式。 Elman網(wǎng)絡(luò) Elman網(wǎng)絡(luò)是一種基本的遞歸神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu),由Elman于1990年提出。其結(jié)構(gòu)主要包括輸入層、隱藏層和輸出層,其中隱藏層具有時間延遲單元,可以存儲一時刻的隱藏狀態(tài)。Elman網(wǎng)絡(luò)的基本原理是
    的頭像 發(fā)表于 07-05 09:32 ?303次閱讀