二叉樹的前中後和層序遍歷詳細圖解(遞迴和非遞迴寫法)
我家門前有兩棵樹,一棵是二叉樹,另一棵也是二叉樹。
遍歷一棵二叉樹常用的有四種方法,前序(PreOrder)、中序(InOrder)、後序(PastOrder)還有層序(LevelOrder)。
前中後序三種遍歷方式都是以根節點相對於它的左右孩子的訪問順序定義的。例如根->左->右便是前序遍歷,左->根->右便是中序遍歷,左->右->根便是後序遍歷。
而層序遍歷是一層一層來遍歷的。
樹的前中後序遍歷是個遞迴的定義,在遍歷到根節點的左/右子樹時,也要遵循前/中/後序遍歷的順序,例如下面這棵樹:
前序遍歷:ABDECFG
中序遍歷:DBEAFCG
後序遍歷:DEBFGCA
層序遍歷:ABCDEFG
樹的結點結構體宣告如下:
語言:C語言(為了省事用到了C++的棧,因為C語言要用棧的話要自己重新寫一個出來,就偷了個懶)
編譯器:VS
typedef char DataType;
typedef struct TreeNode{
DataType data;
struct TreeNode *left;
struct TreeNode *right;
}TreeNode;
前序遍歷(先序遍歷)
對於一棵樹的前序遍歷,遞迴的寫法是最簡單的(寫起來),就是將一個大的問題轉化為幾個小的子問題,直到子問題可以很容易求解,最後將子問題的解組合起來就是大問題的解。
前序訪問的遞迴寫法
先放程式碼,如果看完覺得不太清楚可以看看下面的詳細步驟圖解。
void PreOrder(const TreeNode *root)
{
if (root == NULL) //若結點為空
{
printf("# ");
return;
}
printf("%c ", root->data); //輸出根節點的值
PreOrder(root->left); //前序訪問左子樹
PreOrder(root-> right); //前序訪問右子樹
}
比如說還是上面的這顆樹:
- 訪問根節點
- 訪問左子樹
走到這裡之後發現根節點的左孩子還是一棵子樹,那就將訪問這棵子樹看作是遍歷整顆樹的一個子問題,遍歷這棵子樹的方法和遍歷整顆樹的方法是一樣的。
然後繼續訪問它的左子樹:
為了理解起來方便一點,我在這裡加上了它的兩個為空的左右孩子
然後發現這(可能)還是一棵子樹,就繼續用這種方法來對待這顆子樹,就是繼續訪問它的左子樹:
發現這是一個空節點,那就直接返回,去訪問它的右子樹:
發現還是一個空節點,那麼繼續返回,這時候D和它的左右孩子結點都訪問過了,繼續返回,應該訪問B的右子樹了。
然後就和D結點一樣的處理方法,->左孩子,發現是空,返回->右孩子,發現還是空,繼續返回,發現這時候B的左右孩子都訪問過了,繼續返回。 - 訪問右子樹
然後和處理A的左子樹的方法一樣,最後訪問到G結點的右子樹時,發現是空,就返回,這時候樹的所有節點都已經訪問過了,所以可以一路返回到A結點的右子樹完的地方,整個遞迴就結束了。
最後輸出的前序訪問序列便是:ABDECFG
前序訪問的非遞迴寫法
還是先上程式碼:
void PreOrderLoop(TreeNode *root)
{
std::stack<TreeNode *> s;
TreeNode *cur, *top;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
printf("%c ", cur->data);
s.push(cur);
cur = cur->left;
}
top = s.top();
s.pop();
cur = top->right;
}
}
非遞迴的寫法比遞迴寫法要麻煩一點,要用到棧來儲存樹的結點,在理解非遞迴方法的時候要重點理解棧中儲存的元素的共同點是什麼,在前序訪問中,棧中元素都是自己和自己的左孩子都訪問過了,而右孩子還沒有訪問到的節點,如果不太懂可以看下面的詳細步驟圖解。
- 首先我們要用一個指標(cur)來指向當前訪問的結點
發現這個節點不為空,就將它的資料輸出,然後將這個節點的地址(圖上的棧中寫了節點的值是為了便於理解,實際上棧中儲存的是節點地址)壓棧。
再去訪問它的左子樹,發現左孩子結點依舊不為空,繼續輸出並壓棧。
同理壓棧D節點
然後訪問D的左孩子,發現為空,便從棧中拿出棧頂結點top,讓cur = top->right,便訪問到了D的右孩子。
發現D的右孩子還是為空,這個看一下棧,發現棧不為空,說明還存在右孩子沒被訪問過的節點,就繼續從棧中拿出棧頂結點top,讓cur = top->right,便訪問到了B的右孩子。
B的右孩子處理方法和D一樣,然後再從棧中拿出A節點,去訪問A的右孩子C,在訪問到G節點的右孩子之後,發現當前節點cur為空,棧中也沒有元素可以取出來了,這時候就代表整棵樹都被訪問過了,便結束迴圈。
最後輸出的前序訪問序列便是:ABDECFG
中序遍歷
對於一棵樹的中序遍歷,和前序一樣,可以分為遞迴遍歷和非遞迴遍歷,遞迴遍歷是相對簡單的,還是子問題思想,將一個大問題分解,直到可以解決,最後解決整個大問題。
中序遍歷的遞迴寫法
還是先上程式碼:
void InOrder(const TreeNode *root)
{
if (root == NULL) //判斷節點是否為空
{
printf("# ");
return;
}
InOrder(root->left); //中序遍歷左子樹
printf("%c ", root->data); //訪問節點值
InOrder(root->right); //中序遍歷右子樹
}
- 從根節點進入
- 發現根節點不為空,訪問左子樹
發現不為空,繼續訪問左子樹
發現不為空,繼續訪問左子樹
這時root為空了,就返回去訪問它的根節點,剛才的訪問只是路過,並沒有真正地遍歷節點的資訊,在返回途中才是真正地遍歷到了節點的資訊。
訪問到了D節點,下來要訪問的是D的右孩子,因為D的左孩子已經訪問過了。
發現還是空,就返回,而它的根節點D也訪問過了,那麼就繼續返回,該訪問D節點的父節點B了。
B訪問過後下來要訪問的是B的右孩子,因為是從B的左子樹回來的路,B的左孩子已經訪問過了。
然後和訪問D一樣,->左孩子,為空,返回訪問根節點E,->右孩子,為空(這部分就不畫了,和D節點的訪問是一樣的),最後返回,B已經訪問過了,就繼續返回,至此,整顆樹的左子樹訪問完了。
3. 訪問B的根節點A
4. 遍歷A的右子樹
遍歷右子樹的過程和左子樹一樣,還是左->根->右的中序遍歷下去,直到遍歷到G的右孩子,發現為空,就返回,因為右子樹都遍歷過了,所以可以一直返回到root為A節點的那一層遞迴,整個遍歷結束。
最後輸出的中序訪問序列為:DBEAFCG
非遞迴寫法
中序訪問的非遞迴寫法和前序一樣,都要用到一個棧來輔助儲存,不一樣的地方在於前序訪問時,棧中儲存的元素是右子樹還沒有被訪問到的節點的地址,而中序訪問時棧中儲存的元素是節點自身和它的右子樹都沒有被訪問到的節點地址。
先上程式碼:
void InOrderLoop(TreeNode *root)
{
std::stack<TreeNode *> s;
TreeNode *cur;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
s.push(cur);
cur = cur->left;
}
cur = s.top();
s.pop();
printf("%c ", cur->data);
cur = cur->right;
}
}
- cur指標一路沿著最左邊往下訪問,路過的節點全部壓棧,直到遇到空節點
- 從棧中取出棧頂節點top,輸出棧頂結點的值並使cur = top->right,從第一步開始去遍歷top的右子樹。
遍歷完之後,cur走到了D節點的右孩子,發現cur 為空,但棧中還有元素,就重複第二步
這時候,cur走到了E節點的右孩子,發現cur 為空,但棧中還有元素,就繼續重複第二步,之後cur = top->right,cur指標繼續去遍歷A節點的右子樹,從第一步開始
訪問到F的左孩子節點發現是空,這時候棧中還有元素,就重複第二步
照這個規則依次訪問下去,最後會訪問到G節點的右孩子,這時候cur為空,棧也空了,就代表所有節點已經遍歷完了,就結束迴圈,遍歷完成。
最後輸出的中序訪問序列為:DBEAFCG
後序遍歷
後序遍歷還是分遞迴版本和非遞迴版本,後序遍歷的遞迴版本和前序中序很相似,就是輸出根節點值的時機不同,而後序遍歷的非遞迴版本則要比前序和中序的要難一些,因為在返回根節點時要分從左子樹返回和右子樹返回兩種情況,從左子樹返回時不輸出,從右子樹返回時才需要輸出根節點的值。
遞迴寫法
先上程式碼:
void PostOrder(TreeNode *root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
後序遍歷的遞迴版本和前中序非常相似,就是輸出根節點值的時機不同,詳細圖解這裡就不畫了,可以聯絡前中序的遞迴版本來理解。
後序遍歷的非遞迴寫法
後序遍歷的非遞迴同樣要藉助一個棧來儲存元素,棧中儲存的元素是它的右子樹和自身都沒有被遍歷到的節點,與中序遍歷不同的是先訪問右子樹,在回來的時候再輸出根節點的值。需要多一個last指標指向上一次訪問到的節點,用來確認是從根節點的左子樹返回的還是從右子樹返回的。
先上程式碼:
void PostOrderLoop(TreeNode *root)
{
std::stack<TreeNode *> s;
TreeNode *cur, *top, *last = NULL;
cur = root;
while (cur != NULL || !s.empty())
{
while (cur != NULL)
{
s.push(cur);
cur = cur->left;
}
top = s.top();
if (top->right == NULL || top->right == last){
s.pop();
printf("%c ", top->data);
last = top;
}
else {
cur = top->right;
}
}
}
- 還是沿著左子樹一路往下走,將路過的節點都壓棧,直到走到空節點。
- 然後從棧中看一下棧頂元素(只看一眼,用top指標記下,先不出棧),如果top節點沒有右子樹,或者last等於top的右孩子,說明top的右子樹不存在或者遍歷過了,就輸出top節點的值,並將棧頂元素pop掉(出棧),反之則是從左子樹回到根節點的,接下來要去右子樹。
如圖,top的右孩子為空,說明右子樹不存在,就可以輸出top的值並pop掉棧頂了,這時候用last指標記下top指向的節點,代表上一次處理的節點。(這一過程cur始終沒有動,一直指向空)
繼續從棧頂看一個元素記為top,然後發現top的右孩子不為空,而且last也不等於top->right,就使cur = top->right,回到第一步,用同樣的方法來處理top的右子樹,下一次回來的時候,last指標指向的是E節點。
這時候發現top的右孩子不為空,但是last等於top->right,說明top的右子樹遍歷完成,下一步就要輸出top的值並且將這個節點出棧,下一次再從棧中看一個棧頂元素A即為top。
這時候再比較,發現top的right不為空,而且last也不等於top->right,說明top有右子樹並且還沒有遍歷過,就讓cur = top->right,回到第一步用同樣的方法來遍歷A的右子樹。
到最後,cur訪問到了G的左孩子,而top也一路出棧到了A節點,發現cur為空,並且棧中也為空,這時候便代表整個樹已經遍歷完成,結束迴圈。
最後輸出的中序訪問序列為:DEBFGCA
層序遍歷
層序遍歷是比較接近人的思維方式的一種遍歷方法,將二叉樹的每一層分別遍歷,直到最後的葉子節點被全部遍歷完,這裡要用到的輔助資料結構是佇列,佇列具有先進先出的性質。
上程式碼:
void LevelOrder(TreeNode *root)
{
std::queue<TreeNode *> q;
TreeNode *front;
if (root == NULL)return;
q.push(root);
while (!q.empty())
{
front = q.front();
q.pop();
if (front->left)
q.push(front->left);
if (front->right)
q.push(front->right);
printf("%c ", front->data);
}
}
層序遍歷的思路是,建立一個佇列,先將根節點(A)入隊,然後用front指標將根節點記下來,再將根節點出隊,接下來看front節點(也就是剛才的根節點)有沒有左孩子或右孩子,如果有,先左(B)後右(C)入隊,最後輸出front節點的值,只要佇列還不為空,就說明還沒有遍歷完,就進行下一次迴圈,這時的隊頭元素(front)則為剛才入隊的左孩子(B),然後front出隊,再把它的左右孩子拉進來(如果有),因為佇列的先進先出性質,B的左右孩子DE是排在C後面的,然後輸出B,下一次迴圈將會拉人C的左右孩子FG,最後因為FG沒有左右孩子,一直出隊,沒有入隊元素,佇列遲早會變為空,當佇列為空時,整顆樹就層序遍歷完成了,結束迴圈。
- 根節點入隊,並用front指標標記
- 隊頭出隊,並將左右孩子拉進佇列
重複1,2
- 直到佇列為空
這時候便代表整個樹遍歷完成,結束迴圈。
最後輸出的層序訪問序列為:ABCDEF
編輯於2018-8-27 16:55:37