在前兩篇文章二叉樹和二叉搜尋樹中已經涉及到了二叉樹的三種遍歷。遞迴寫法,只要理解思想,幾行**。可是非遞迴寫法卻很不容易。這裡特地總結下,透徹解析它們的非遞迴寫法。其中,中序遍歷的非遞迴寫法最簡單,後序遍歷最難。我們的討論基礎是這樣的:
//binary tree node
typedef struct node
btnode;
首先,有一點是明確的:非遞迴寫法一定會用到棧,這個應該不用太多的解釋。我們先看中序遍歷:
分析中序遍歷的遞迴定義:先左子樹,後根節點,再右子樹。如何寫非遞迴**呢?一句話:讓**跟著思維走。我們的思維是什麼?思維就是中序遍歷的路徑。假設,你面前有一棵二叉樹,現要求你寫出它的中序遍歷序列。如果你對中序遍歷理解透徹的話,你肯定先找到左子樹的最下邊的節點。那麼下面的**就是理所當然的:
中序**段(i)
btnode* p = root; //p指向樹根
stack s; //stl中的棧
//一直遍歷到左子樹最下邊,邊遍歷邊儲存根節點到棧中
while (p)
儲存一路走過的根節點的理由是:中序遍歷的需要,遍歷完左子樹後,需要借助根節點進入右子樹。**走到這裡,指標p為空,此時無非兩種情況:
說明:上圖中只給出了必要的節點和邊,其它的邊和節點與討論無關,不必畫出。你可能認為圖a中最近儲存節點算不得是根節點。如果你看過樹、二叉樹基礎,使用擴充二叉樹的概念,就可以解釋。總之,不用糾結這個沒有意義問題。
整個二叉樹只有乙個根節點的情況可以劃到圖a。
仔細想想,二叉樹的左子樹,最下邊是不是上圖兩種情況?不管怎樣,此時都要出棧,並訪問該節點。這個節點就是中序序列的第乙個節點。根據我們的思維,**應該是這樣:
p = s.top();
s.pop();
cout << p->data;
我們的思維接著走,兩圖情形不同得區別對待:
1.圖a中訪問的是乙個左孩子,按中序遍歷順序,接下來應訪問它的根節點。也就是圖a中的另乙個節點,高興的是它已被儲存在棧中。我們只需這樣的**和上一步一樣的**:
p = s.top();
s.pop();
cout << p->data;
左孩子和根都訪問完了,接著就是右孩子了,對吧。接下來只需一句**:p=p->rchild;在右子樹中,又會新一輪的**段(i)、**段(ii)……直到棧空且p空。
2.再看圖b,由於沒有左孩子,根節點就是中序序列中第乙個,然後直接是進入右子樹:p=p->rchild;在右子樹中,又會新一輪的**段(i)、**段(ii)……直到棧空且p空。
思維到這裡,似乎很不清晰,真的要區分嗎?根據圖a接下來的**段(ii)這樣的:
p = s.top();
s.pop();
cout << p->data;
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;
根據圖b,**段(ii)又是這樣的:
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;
我們可小結下:遍歷過程是個迴圈,並且按**段(i)、**段(ii)構成一次迴圈體,迴圈直到棧空且p空為止。
不同的處理方法很讓人抓狂,可統一處理嗎?真的是可以的!回顧擴充二叉樹,是不是每個節點都可以看成是根節點呢?那麼,**只需統一寫成圖b的這種形式。也就是說**段(ii)統一是這樣的:
中序**段(ii)
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;
口說無憑,得經的過理論檢驗。
圖a的**段(ii)也可寫成圖b的理由是:由於是葉子節點,p=-=p->rchild;之後p肯定為空。為空,還需經過新一輪的**段(i)嗎?顯然不需。(因為不滿足迴圈條件)那就直接進入**段(ii)。看!最後還是一樣的吧。還是連續出棧兩次。看到這裡,要仔細想想哦!相信你一定會明白的。
這時寫出遍歷迴圈體就不難了:
btnode* p = root;
stack s;
while (!s.empty() || p)
//**段(ii)當p為空時,說明已經到達左子樹最下邊,這時需要出棧了
if (!s.empty())
}仔細想想,上述**是不是根據我們的思維走向而寫出來的呢?再加上邊界條件的檢測,中序遍歷非遞迴形式的完整**是這樣的:
中序遍歷**一
//中序遍歷
void inorderwithoutrecursion1(btnode* root)
//當p為空時,說明已經到達左子樹最下邊,這時需要出棧了
if (!s.empty())}}
恭喜你,你已經完成了中序遍歷非遞迴形式的**了。回顧一下難嗎?
接下來的這份**,本質上是一樣的,相信不用我解釋,你也能看懂的。
中序遍歷**二
//中序遍歷
void inorderwithoutrecursion2(btnode* root)
else}}
分析前序遍歷的遞迴定義:先根節點,後左子樹,再右子樹。有了中序遍歷的基礎,不用我再像中序遍歷那樣引導了吧。
首先,我們遍歷左子樹,邊遍歷邊列印,並把根節點存入棧中,以後需借助這些節點進入右子樹開啟新一輪的迴圈。還得重複一句:所有的節點都可看做是根節點。根據思維走向,寫出**段(i):
前序**段(i)
//邊遍歷邊列印,並存入棧中,以後需要借助這些根節點(不要懷疑這種說法哦)進入右子樹
while (p)
接下來就是:出棧,根據棧頂節點進入右子樹。
前序**段(ii)
//當p為空時,說明根和左子樹都遍歷完了,該進入右子樹了
if (!s.empty())
同樣地,**段(i)(ii)構成了一次完整的迴圈體。至此,不難寫出完整的前序遍歷的非遞迴寫法。
前序遍歷**一
void preorderwithoutrecursion1(btnode* root)
//當p為空時,說明根和左子樹都遍歷完了,該進入右子樹了
if (!s.empty())
}cout << endl;
}下面給出,本質是一樣的另一段**:
前序遍歷**二
//前序遍歷
void preorderwithoutrecursion2(btnode* root)
else
}cout << endl;
}在二叉樹中使用的是這樣的寫法,略有差別,本質上也是一樣的:
前序遍歷**三
void preorderwithoutrecursion3(btnode* root)
} cout << endl;
}最後進入最難的後序遍歷:分析
後序遍歷遞迴定義:先左子樹,後右子樹,再根節點。後序遍歷的難點在於:需要判斷上次訪問的節點是位於左子樹,還是右子樹。若是位於左子樹,則需跳過根節點,先進入右子樹,再回頭訪問根節點;若是位於右子樹,則直接訪問根節點。直接看**,**中有詳細的注釋。
後序遍歷**
//後序遍歷
void postorderwithoutrecursion(btnode* root)
while (!s.empty())
/*這裡的else語句可換成帶條件的else if:
else if (pcur->lchild == plastvisit)//若左子樹剛被訪問過,則需先進入右子樹(根節點需再次入棧)
因為:上面的條件沒通過就一定是下面的條件滿足。仔細想想!
*/else
}} cout << endl;
}思維和**之間總是有巨大的鴻溝。通常是思維正確,清楚,但卻不易寫出正確的**。要想越過這鴻溝,只有多嘗試、多借鑑,別無它法。
專欄目錄:資料結構與演算法目錄
二叉樹前序中序後序的非遞迴遍歷
1前序遍歷 按照根左右的順序進行遍歷,首先訪問根節點,若左孩子非空,訪問左子樹 右孩子非空訪問右子樹。以此規則遍歷整個二叉樹 1 當前點入棧,輸出該節點 2 如當前節點左孩子非空,訪問該節點的左孩子回到 1 3 若當前節點左孩子為空,訪問該節點右孩子回到 1 stacks vectorres tre...
二叉樹 前序遍歷 中序遍歷 後序遍歷
前序遍歷 dlr 是二叉樹遍歷的一種,也叫做先跟遍歷,先序遍歷,前序周遊,可記做根左右。前序遍歷首先訪問根節點然後遍歷左子樹,最後遍歷右子樹。前序遍歷首先訪問根節點然後遍歷左子樹,最後遍歷右子樹。在遍歷左 右子樹時,仍然先訪問根節點,然後遍歷左子樹,最後遍歷右子樹。若二叉樹為空則結束返回,否則 1 ...
二叉樹前序 中序 後序遍歷非迭代解法
經常會有面試官,讓你手撕二叉樹的前序 中序 後序遍歷,當你簡單得寫了遞迴的方法,前序和中序遍歷,解決方法類似,都是使用棧解決的。後序遍歷稍微複雜一點。二叉樹前序遍歷 class solution p stack.pop p p.right return list 二叉樹中序遍歷 class solu...