俗話說:學如逆水行舟,不進則退;心似平原走馬,易放難收。這句話對程式設計師而言,體會更深。這行已經越來越捲了,時刻準備著,?。 二叉樹,在面試中,已是必備的開胃菜。而在二叉樹相關的面試題目中,遍歷更是常考題目。本文將從二叉樹的遍歷角度入手,從遞迴和非遞迴角度來分析和講解二叉樹的遍歷。
遍歷
❝二叉樹的遍歷是指從根節點出發,按照某種次序依次訪問二叉樹中的所有節點,使每個節點被且僅被訪問一次。
❞
二叉樹的遍歷,有「先序遍歷」、「中序遍歷」以及「後續遍歷」三種。
圖一
上面三種遍歷方式中的先序、中序以及後序三種方式,是父節點相對於子節點來說的。如果父節點先於子節點,那麼就是先序遍歷。如果子節點先於父節點,那麼就是後序遍歷。而對於子節點來說,如果先左節點,然後是父節點,然後再是右節點,那麼就是中序遍歷。
如【圖一】所示二叉樹。其三種遍歷結果如下:
先序遍歷: A->B->D->E->C->F->G
中序遍歷: D->B->E->A->F->C->G
後續遍歷: D->E->B->F->G->C->A
為了便於理解程式碼,我們先定義下樹的節點定義:
struct TreeNode {
TreeNode *left;
TreeNode *right;
int val;
};
先序遍歷
❝定義:先訪問父節點,然後遍歷左子樹,最後遍歷右子樹。
❞
遞迴
相信遞迴遍歷大家都會很容易寫出來,且bugfree。因為實現程式碼很簡單。
圖二 先序遍歷
在上圖【圖二】中,使用遞迴遍歷,就是將其左子樹和右子樹也當做一棵樹來進行處理。程式碼如下:
void PreOrder(TreeNode *root) {
if (!root) {
return;
}
// 遍歷根節點(此處僅為輸出,讀者也可以根據實際需要進行處理,比如儲存等)
std::cout << root->val << std::endl;
// 遍歷左子樹
PreOrder(root->left);
// 遍歷右子樹
PreOrder(root->right);
}
非遞迴
在非遞迴操作中,我們仍然是按照先訪問根節點,然後遍歷左子樹,接著遍歷右子樹的方式。
圖三 先序遍歷
- 到達節點A,訪問節點A,開始遍歷A的左子樹
- 到達節點B,訪問節點B,開始遍歷B的左子樹
- 到達節點D,訪問節點D,因為節點D無子樹,因此節點D遍歷完成
- 節點D遍歷完成,意味著節點B的左子樹遍歷完成,因此接著遍歷節點B的右子樹
- 到達節點E,訪問節點E。因為節點E無子樹,因此節點E遍歷完成
- 節點E遍歷完成,意味著節點B的右子樹遍歷完成,也預示著節點B的子樹遍歷完成
- 開始遍歷節點A的右子樹
- 到達節點C,訪問節點C。開始遍歷C的左子樹
- 到達節點F,訪問節點F。因為節點F無子樹,因此節點F遍歷完成
- 節點F遍歷完成,意味著節點C的左子樹遍歷完成,因此開始遍歷節點C的右子樹
- 到的節點G,訪問節點G。因為節點G無子樹,因此節點G遍歷完成
- 節點G遍歷完成,意味著節點C的右子樹遍歷完成,進而預示著節點C遍歷完成
- 節點C遍歷完成,意味著節點A的右子樹遍歷完成,進而意味著節點A遍歷完成,因此以A為根節點的樹遍歷完成。
用非遞迴方式遍歷二叉樹,需要引入額外的資料結構棧(stack),其基本流程如下: 1、申請一個棧stack,然後將頭節點壓入stack中。
2、從stack中彈出棧頂節點,列印
3、將其右孩子節點(不為空的話)先壓入stack中
4、將其左孩子節點(不為空的話)壓入stack中。
5、不斷重複步驟2、3、4,直到stack為空,全部過程結束。
程式碼如下:
void PreOrder(TreeNode *root) {
if (!root) {
return;
}
std::stack<TreeNode*> s;
s.push(root); // 步驟1
while (!s.empty()) {
auto t = s.top();
s.pop();//出棧
std::cout << t->val << std::endl; // 訪問節點
if (t->right) {
s.push(t->right); // 對應步驟3
}
if (t->left) {
s.push(t->left); // 對應步驟4
}
}
}
中序遍歷
❝定義:先遍歷左子樹,訪問根節點,遍歷右子樹
❞
遞迴
圖四 中序遍歷
在上圖【圖四】中,使用遞迴遍歷,就是將其左子樹和右子樹也當做一棵樹來進行處理。程式碼如下:
void InOrder(TreeNode *root) {
if (!root) {
return;
}
// 遍歷左子樹
InOrder(root->left);
// 遍歷根節點(此處僅為輸出,讀者也可以根據實際需要進行處理,比如儲存等)
std::cout << root->val << std::endl;
// 遍歷右子樹
InOrder(root->right);
}
上述中序遍歷的遞迴程式碼,相比於先序遍歷,只是將訪問根節點的行為放在了遍歷左右子樹之間。
非遞迴
在非遞迴操作中,我們仍然是按照先遍歷左子樹,然後訪問根節點,最後遍歷右子樹的方式。
圖五 中序遍歷
- 到達節點A,節點A有左子樹,遍歷節點A的左子樹
- 到達節點B,節點B有左子樹,遍歷節點B的左子樹
- 到達節點D,節點D無子樹,訪問D節點
- 由於D無子樹,意味著B的左子樹遍歷完成,那麼就回到B節點
- 訪問B節點,遍歷B節點的右子樹
- 到達節點E,節點E無子樹,訪問節點E
- E節點遍歷完成,意味著以B為根的子樹遍歷完成,回到A節點
- 到達A節點,訪問A節點,遍歷A節點的右子樹
- 到達C節點,遍歷C節點的左子樹
- 到達F節點,因為F節點無子樹,因此訪問F節點。
- 由於F節點無子樹,意味著C節點的左子樹遍歷完成,回到C節點
- 到達C節點,訪問C節點,遍歷C的右子樹
- 到達節點G,由於G無子樹,因為訪問節點G
- G節點遍歷完成,意味著C節點的右子樹遍歷完成,進而意味著A節點的右子樹遍歷完成,從意味著以A節點為根的二叉樹遍歷完成。
中序遍歷,同樣需要額外的輔助資料結構棧。
- 將根節點放入棧 2、如果根節點有左子樹,則將左子樹的根節點放入棧 3、重複步驟1和2.繼續遍歷左子樹 4、從棧中彈出節點,進行訪問,然後遍歷右子樹(重複步驟1和2) 5、如果棧為空,則遍歷完成
程式碼如下:
void InOrder(TreeNode *root) {
if (!root) {
return;
}
std::stack<TreeNode*> s;
auto p = root;
while (!s.empty() || p) {
if (p) { // 步驟1和2
s.push(p);
p = p->left;
} else { // 步驟4
auto t = s.top();
std::cout << t->val << std::endl;
p = t->right;
}
}
}
後續遍歷
❝定義:先遍歷左子樹,再遍歷右子樹,最後訪問根節點
❞
遞迴
圖六 後序遍歷
void PostOrder(TreeNode *root) {
if (!root) {
return;
}
// 遍歷左子樹
PostOrder(root->left);
// 遍歷右子樹
PostOrder(root->right);
// 遍歷根節點(此處僅為輸出,讀者也可以根據實際需要進行處理,比如儲存等)
std::cout << root->val << std::endl;
}
上面就是後續遍歷的遞迴寫法,比較寫先序遍歷、中序遍歷以及後續遍歷三者的遞迴遍歷寫法,大部分程式碼是一樣的,唯一的區別就是「訪問根節點的程式碼位置」不一樣:
先序遍歷:「先訪問根節點,然後遍歷左子樹,最後遍歷右子樹」 中序遍歷:「先遍歷左子樹,然後訪問根節點,最後遍歷右子樹」 後序遍歷:「先遍歷左子樹,然後遍歷右子樹,最後訪問根節點」
非遞迴
在非遞迴操作中,我們仍然是按照先遍歷左子樹,然後訪問根節點,最後遍歷右子樹的方式。
圖七 後續遍歷
- 到達節點A,遍歷A的左子樹
- 到達節點B,遍歷B的左子樹
- 到達節點D,由於D無子樹,則訪問節點D
- 節點D無子樹,意味著節點B的左子樹遍歷完成,接著遍歷B的右子樹
- 到達節點E,由於E無子樹,則訪問節點E
- 節點E無子樹,意味著節點B的右子樹遍歷完成,接回到節點B
- 訪問節點B,回到節點B的根節點A
- 到達節點A,訪問節點A的右子樹
- 到達節點C,遍歷節點C的左子樹
- 到達節點F,由於節點F無子樹,因此訪問節點F
- 節點F訪問完成,意味著C節點的左子樹遍歷完成,因此回到節點C
- 到達節點C,遍歷節點C的右子樹
- 到達節點G,由於節點G無子樹,因為訪問節點G
- 節點G訪問完成,意味著C節點的右子樹遍歷完成,回到節點C
- 到達節點C,訪問節點C
- 節點C遍歷完成,意味著節點A的右子樹遍歷完成,回到節點A
- 節點A的右子樹遍歷完成,訪問節點A
用非遞迴方式遍歷二叉樹,需要引入額外的資料結構棧(stack),其基本流程如下: 1、申請兩個棧stack,然後將頭節點壓入指定stack中。
2、從stack中彈出棧頂節點,放入另外一個棧中
3、將其左孩子節點(不為空的話)先壓入stack中
4、將其右孩子節點(不為空的話)壓入stack中。
5、不斷重複步驟2、3、4,直到stack為空。
6、重複訪問另外一個棧,直至棧空
void PostOrder(TreeNode *root) {
if (!root) {
return;
}
std::stack<TreeNode*> s1;
std::stack<TreeNode*> s2;
s1.push(root);
while (!s1.empty()) {
auto t = s1.top();
s1.pop();
s2.push(t);
if (t->left) {
s1.push(t->left);
}
if (t->right) {
s1.push(t->right);
}
}
while (!s2.empty()) {
auto t = s2.top();
s2.pop();
std::cout << t->val << std::endl;
}
}
結語
對於二叉樹來說,所謂的遍歷,是指沿著某條路線依次訪問每個節點,且均只做一次訪問。二叉樹的遍歷,是面試中常面演算法之一,一定要把其弄懂,必要的時候,需要背誦,乃至做到肌肉記憶。