之前經(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í)行。
如果從遞歸代碼上來(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
出棧,返回值傳給B
,B
執(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)):
簡(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)注明出處。
-
算法
+關(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)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論