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

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

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

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

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

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

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

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

前文BFS 算法框架詳解是利用隊(duì)列迭代地遍歷二叉樹(shù),不過(guò)使用的是層級(jí)遍歷,沒(méi)有遞歸遍歷中的前中后序之分。

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

首先我想說(shuō),遞歸改迭代從實(shí)用性的角度講是沒(méi)什么意義的,明明可以寫(xiě)遞歸解法,為什么非要改成迭代的方式?

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

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

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

Listtraverse(TreeNoderoot);

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

而我想要的是一個(gè)萬(wàn)能的模板,可以把一切二叉樹(shù)遞歸算法都改成迭代。

換句話(huà)說(shuō),類(lèi)似二叉樹(shù)的遞歸框架:

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

迭代框架也應(yīng)該有前中后序代碼的位置:

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

我如果想把任何一道二叉樹(shù)遞歸解法改成迭代,直接把遞歸解法中前中后序?qū)?yīng)位置的代碼復(fù)制粘貼到迭代框架里,就可以直接運(yùn)行,得到正確的結(jié)果。

理論上,所有遞歸算法都可以利用棧改成迭代的形式,因?yàn)橛?jì)算機(jī)本質(zhì)上就是借助棧來(lái)迭代地執(zhí)行遞歸函數(shù)的。

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

遞歸框架改為迭代

參加過(guò)我的二叉樹(shù)專(zhuān)項(xiàng)訓(xùn)練的讀者應(yīng)該知道,二叉樹(shù)的遞歸框架中,前中后序遍歷位置就是幾個(gè)特殊的時(shí)間點(diǎn):

前序遍歷位置的代碼,會(huì)在剛遍歷到當(dāng)前節(jié)點(diǎn)root,遍歷root的左右子樹(shù)之前執(zhí)行;

中序遍歷位置的代碼,會(huì)在在遍歷完當(dāng)前節(jié)點(diǎn)root的左子樹(shù),即將開(kāi)始遍歷root的右子樹(shù)的時(shí)候執(zhí)行;

后序遍歷位置的代碼,會(huì)在遍歷完以當(dāng)前節(jié)點(diǎn)root為根的整棵子樹(shù)之后執(zhí)行。

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

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

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

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

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

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

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

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

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

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

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

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

簡(jiǎn)單說(shuō)就是這樣一個(gè)流程:

1、拿到一個(gè)節(jié)點(diǎn),就一路向左遍歷(因?yàn)?code style="margin-right:2px;margin-left:2px;padding:2px 4px;font-size:inherit;line-height:inherit;color:rgb(233,105,0);background:rgb(248,248,248);">traverse(root.left)排在前面),把路上的節(jié)點(diǎn)都?jí)旱綏@?/strong>。

2、往左走到頭之后就開(kāi)始退棧,看看棧頂節(jié)點(diǎn)的右指針,非空的話(huà)就重復(fù)第 1 步。

寫(xiě)成迭代代碼就是這樣:

privateStackstk=newStack<>();

publicListtraverse(TreeNoderoot){
pushLeftBranch(root);

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

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

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

迭代代碼框架

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

當(dāng)我從棧中拿出一個(gè)節(jié)點(diǎn)p,我應(yīng)該想辦法搞清楚這個(gè)節(jié)點(diǎn)p左右子樹(shù)的遍歷情況

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

如果p的左子樹(shù)被遍歷過(guò)了,而右子樹(shù)沒(méi)有被遍歷過(guò),那么現(xiàn)在對(duì)p進(jìn)行操作就屬于中序遍歷代碼。

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

上述邏輯寫(xiě)成偽碼如下:

privateStackstk=newStack<>();

publicListtraverse(TreeNoderoot){
pushLeftBranch(root);

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

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

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

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

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

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

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

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

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

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

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

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

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

PS:visited指針初始化指向一個(gè)新 new 出來(lái)的二叉樹(shù)節(jié)點(diǎn),相當(dāng)于一個(gè)特殊值,目的是避免和輸入二叉樹(shù)中的節(jié)點(diǎn)重復(fù)。

只需把遞歸算法中的前中后序位置的代碼復(fù)制粘貼到上述框架的對(duì)應(yīng)位置,就可以把任意遞歸的二叉樹(shù)算法改寫(xiě)成迭代形式了。

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

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;
}
}

當(dāng)然,任何一個(gè)二叉樹(shù)的算法,如果你想把遞歸改成迭代,都可以套用這個(gè)框架,只要把遞歸的前中后序位置的代碼對(duì)應(yīng)過(guò)來(lái)就行了。

迭代解法到這里就搞定了,不過(guò)我還是想強(qiáng)調(diào),除了 BFS 層級(jí)遍歷之外,二叉樹(shù)的題目還是用遞歸的方式來(lái)做,因?yàn)檫f歸是最符合二叉樹(shù)結(jié)構(gòu)特點(diǎn)的。

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

原文標(biāo)題:二叉樹(shù)八股文:遞歸改迭代通用模板

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

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

    關(guān)注

    23

    文章

    4613

    瀏覽量

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

    關(guān)注

    0

    文章

    108

    瀏覽量

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

    關(guān)注

    3

    文章

    4332

    瀏覽量

    62666

原文標(biāo)題:二叉樹(shù)八股文:遞歸改迭代通用模板

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

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    labview遞歸使用你嘗試過(guò)嗎?

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

    用Kei寫(xiě)程序的時(shí)候,怎么頭文件為AT89X51.H的程序改寫(xiě)成...

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

    《C Primer Plus》讀書(shū)筆記——遞歸

    ("LEVEL %d: n location %p\n" , n, &n);}輸出如下:遞歸的基本原理每級(jí)遞歸都使用其私有變量(如例子的n)每次函數(shù)調(diào)用都返回一級(jí)(調(diào)用他那級(jí)
    發(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,實(shí)現(xiàn)自身調(diào)用看見(jiàn)下圖NI自帶遞歸方法二、如果靜態(tài)調(diào)用改成直接調(diào)用自身也可得到相同的結(jié)果,而且
    發(fā)表于 05-18 10:36

    遞歸最小二乘法

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

    LabVIEW中使用遞歸算法

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

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

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

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

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

    如何把C++的源程序改寫(xiě)成C語(yǔ)言

    第一種是C++的面向?qū)ο筇卣魅サ簦热坷斫庠创a的邏輯,然后改寫(xiě);第二種是在C中保留面向?qū)ο蟮牟糠痔卣?,用結(jié)構(gòu)體實(shí)現(xiàn)類(lèi)的功能。
    的頭像 發(fā)表于 05-14 10:08 ?2927次閱讀
    如何把C++的源程序<b class='flag-5'>改寫(xiě)成</b>C語(yǔ)言

    C語(yǔ)言編程如何求出二叉樹(shù)后序遍歷

    題目 已知二叉樹(shù)前序?yàn)?ABDFGCEH 后序序列為 BFDGACEH ,要求輸出后序遍歷為 FGDBHECA 大體思路 又先序得出根,先序的根后為左樹(shù)一部分,我們?cè)僭?b class='flag-5'>中序序列里找到先序的根,此處
    的頭像 發(fā)表于 08-23 11:04 ?3925次閱讀

    如何求遞歸算法的時(shí)間復(fù)雜度

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

    Python支持遞歸函數(shù)

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

    如何將Pytorch自訓(xùn)練模型變成OpenVINO IR模型形式

    本文章依次介紹如何將Pytorch自訓(xùn)練模型經(jīng)過(guò)一系列變換變成OpenVINO IR模型形式,而后使用OpenVINO Python API 對(duì)IR模型進(jìn)行推理,并將推理結(jié)果通過(guò)OpenCV API顯示在實(shí)時(shí)畫(huà)面上。
    的頭像 發(fā)表于 06-07 09:31 ?2028次閱讀
    <b class='flag-5'>如何將</b>Pytorch自訓(xùn)練模型變成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)主要包括輸入層、隱藏層和輸出層,其中隱藏層具有時(shí)間延遲單元,可以存儲(chǔ)一時(shí)刻的隱藏狀態(tài)。Elman網(wǎng)絡(luò)的基本原理是
    的頭像 發(fā)表于 07-05 09:32 ?556次閱讀