現(xiàn)在決定把自己很久以前的一些文章重新markdown一下,發(fā)到簡(jiǎn)書來(lái),先從這篇二叉樹的遍歷說(shuō)起的。
大家都知道二叉樹的遍歷分為前序遍歷,中序遍歷,后序遍歷。記得大學(xué)學(xué)習(xí)這一部分的時(shí)候,覺得用遞歸就可以輕易實(shí)現(xiàn),簡(jiǎn)直太簡(jiǎn)單了,所以也就沒有認(rèn)真學(xué),不過(guò)后來(lái)面試的時(shí)候考官要寫一下二叉樹的中序遍歷,而且不能用遞歸,當(dāng)時(shí)就很尷尬了,所以現(xiàn)在把二叉樹的每種遍歷思想和方法都記錄一下,做個(gè)備忘
遞歸的解法來(lái)解決前序中序和后續(xù)的遍歷
這簡(jiǎn)直是太簡(jiǎn)單不過(guò)了
遞歸前序遍歷
public static void preTranverse(TreeNode root){
if(root != null){
visit(root);
preTranverse(root.leftChild);
preTranverse(root.rightChild);
}
}
是不是非常言簡(jiǎn)意賅,遍歷的時(shí)候先訪問(wèn)本身,然后遍歷左節(jié)點(diǎn),接著遍歷右節(jié)點(diǎn),而且簡(jiǎn)單易懂,接下來(lái)直接寫中序遍歷和后續(xù)遍歷,思路是一樣的
遞歸中序遍歷
public static void midTranverse(TreeNode root){
if(root != null){
midTranverse(root.leftChild);
visit(root);
midTranverse(root.rightChild);
}
}
遞歸后續(xù)遍歷
public static void postTranverse(TreeNode root){
if(root != null){
postTranverse(root.leftChild);
postTranverse(root.rightChild);
visit(root);
}
}
這三種寫法都簡(jiǎn)單明了,一看就懂。下面的重點(diǎn)是三種遍歷方式的非遞歸實(shí)現(xiàn)
要理解非遞歸實(shí)現(xiàn),我們就要先清楚三種遍歷的邏輯過(guò)程,以中序遍歷為例:
- 想要訪問(wèn)節(jié)點(diǎn)p,就要先訪問(wèn)p的左子節(jié)點(diǎn)p.leftChild.
- 想要訪問(wèn)p.leftChild,同樣也要先訪問(wèn)p的左子節(jié)點(diǎn)的子節(jié)點(diǎn),p.leftChild.leftChild
- 依次類推,當(dāng)p的左子節(jié)點(diǎn)以及左子節(jié)點(diǎn)的左子節(jié)點(diǎn)都訪問(wèn)完的時(shí)候,我們才可以訪問(wèn)P。這個(gè)時(shí)候P的指向應(yīng)該是原來(lái)的root節(jié)點(diǎn)的最后一個(gè)左子節(jié)點(diǎn)
- 當(dāng)訪問(wèn)完P(guān)的時(shí)候,我們要借助P來(lái)對(duì)P的右節(jié)點(diǎn)進(jìn)行訪問(wèn).
- 不斷重復(fù)上一個(gè)過(guò)程,就可以中序遍歷完整個(gè)二叉樹
知道了這些,我們就可以大致寫出代碼了,代碼中有注釋,可以參考
public static void midTranverse(TreeNode root){
TreeNode p = root;
//stack用來(lái)存放我們第二和第三個(gè)步驟中遍歷到的左子節(jié)點(diǎn),我們要依靠這些左子節(jié)點(diǎn)來(lái)進(jìn)入到他們對(duì)應(yīng)的右樹中去
Stack<TreeNode> s = new Stack();
while(p!= null || !s.isEmpty()){
//按照思路,先讓P節(jié)點(diǎn)的所有左子節(jié)點(diǎn)入棧
while(p!=null){
s.push(p);
p = p.leftChild;
}
//這時(shí)候P應(yīng)該為空,那么我們只能根據(jù)棧內(nèi)是否有元素來(lái)判斷是否要出棧了
if(!s.isEmpty()){
//注意,這里的if不能寫成while,這樣的話相當(dāng)于全部出棧了,又回到了root節(jié)點(diǎn),但是此時(shí)我們僅僅遍歷了root節(jié)點(diǎn)左子樹的所有左節(jié)點(diǎn),還沒有遍歷左子樹的所有右節(jié)點(diǎn)
p=s.pop();
//可以訪問(wèn)p了,因?yàn)檫@時(shí)候P指向root左子樹的最后一個(gè)左子節(jié)點(diǎn)。
visit(p);
//雖然P是左子樹的最后一個(gè)左子節(jié)點(diǎn),但是P可能還會(huì)有右子節(jié)點(diǎn),如果有的話,進(jìn)入p的右子節(jié)點(diǎn)進(jìn)行遍歷,如果沒有的話也沒有關(guān)系,因?yàn)樵谙麓未笱h(huán)中P為空,還會(huì)繼續(xù)取出我們?cè)瓉?lái)?xiàng)?nèi)的元素進(jìn)行遍歷
p=p.rightChild();
}
}
}
完整的代碼就是這樣,我們發(fā)現(xiàn)在外層循環(huán)的時(shí)候內(nèi)層還有循環(huán),這樣效率不是很高,不過(guò)經(jīng)過(guò)我們以上的分析,發(fā)現(xiàn)內(nèi)層循環(huán)其實(shí)是可以省略的,代碼如下
public static void midTranverse(TreeNode root){
TreeNode p = root;
Stack<TreeNode> s = new Stack();
while(p!= null || !s.isEmpty()){
if(p!=null){
s.push(p);
p=p.leftChild;
}else{
p = s.pop();
visit(p);
p=p.rightChild;
}
}
}
代碼簡(jiǎn)潔了很多,基本思路沒有變,都是當(dāng)p不為空或者棧中還有元素的時(shí)候,不斷循環(huán),當(dāng)p不為空的時(shí)候,我們要讓p入棧,并進(jìn)入p的左樹,當(dāng)p為空的時(shí)候,我們出棧,把棧頂元素賦給p,并進(jìn)入p的右樹。
這樣就可以完成整個(gè)二叉樹的中序遍歷了。
前序遍歷和中序遍歷流程差不多,只是visit調(diào)用的實(shí)際不一樣,前序遍歷是在進(jìn)入左樹之前就會(huì)調(diào)用visit方法,中序遍歷是在進(jìn)入右樹之前調(diào)用visit方法
非遞歸的后續(xù)遍歷
后續(xù)遍歷相比于前兩種遍歷困難的地方在于父節(jié)點(diǎn)必須在左右子樹都遍歷完的時(shí)候才能進(jìn)行訪問(wèn),我們需要有一個(gè)新的變量來(lái)記錄是否已經(jīng)訪問(wèn)完右子樹了。
public static void postTranverse(TreeNode root){
TreeNode p = root;
TreeNode lastVisited = null;
Stack<TreeNode> s = new Stack();
//我們?cè)谂袛嗍欠窨梢栽L問(wèn)一個(gè)節(jié)點(diǎn)時(shí),都需要確保該節(jié)點(diǎn)的左子樹和有子樹都已經(jīng)訪問(wèn)完了,為了能夠確定該節(jié)點(diǎn)的左右子樹都訪問(wèn)完了,先進(jìn)入他左子樹上每個(gè)節(jié)點(diǎn)是否都訪問(wèn)過(guò)
while(p!=null){
s.push(p);
p=p.leftChild;
}
while(!s.isEmpty()){
//取出棧頂元素
p=s.pop();
if(p.rightChild == null || p.rightChild == lastVisited){
//當(dāng)p的右子樹為Null或者已經(jīng)被訪問(wèn)過(guò)的時(shí)候,我們才能訪問(wèn)p
visit(p);
lastVisited = p;
}else{
//棧頂?shù)脑噩F(xiàn)在右子樹沒有被訪問(wèn)過(guò),則再次入棧,先不訪問(wèn)該元素
s.push(p);
p=p.rightChild;
//然后去判斷這個(gè)右子樹是否被訪問(wèn)過(guò)
while(p!=null){
s.push(p);
p=p.leftChild;
}
}
}
}