二叉樹的遍歷()是指從根節點出發,按照某種次序依次訪問二叉樹中所有結點,使得每個結點被訪問一次且僅被訪問一次。
這裏有兩個關鍵詞:訪問 和 次序。
訪問其實是要根據實際的需要來確定做什麼,比如對每個結點進行相關計算,輸出打印等,它算是一個抽象操作。在這裏我們可以簡單地假定就是輸出結點的數據信息。
二叉樹的遍歷次序不同於線性結構,一般的線性結構最多也就是從頭至尾、循環、雙向等簡單的遍歷方式。樹的結點之間不存在唯一的前驅和後繼關係,在訪問一個結點後,下一個被訪問的結點面臨着不同的選擇,由於選擇方式的不同,遍歷的次序也就完全不同了。
二叉樹遍歷方法
二叉樹的遍歷方式可以很多,如果我們限制了從左到右的習慣方式,那麼主要就分爲四種:
- 前序遍歷
- 中序遍歷
- 後序遍歷
- 層序遍歷
前序遍歷
規則是若二叉樹爲空,則空操作返回,否則先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹。
如下圖,遍歷順序爲:ABDGHCEIF。
接下來我們以下圖爲例對前序遍歷過程做以分析:
遍歷步驟:
- 先訪問根節點A,結果爲A
- 前序遍歷根節點A的左子樹,左子樹的根節點是B,此時結果爲AB
- 繼續前序遍歷以B爲根節點的左子樹,此時的根節點是D,結果爲ABD
- 繼續遍歷以D爲根節點的左子樹H,此時結果爲ABDH
- 節點H不存在左子樹,則遍歷以D爲根節點的右子樹,此時結果爲ABDHI
- 遞歸遍歷以B爲根節點右子樹E,由於E存在左子樹J,此時結果爲ABDHIE
- E存在左子樹,此時結果爲ABDHIEJ。此時根節點A的左子樹遍歷完成,開始遍歷右子樹
- 右子樹根節點爲C,此時結果爲ABDHIEJC
- 以C爲根節點的子樹存在左節點F,結果變爲ABDHIEJCF
- 節點F不存在子節點,遍歷C的右節點,前序遍歷最後結果爲ABDHIEJCFG
中序遍歷
規則是若樹爲空,則空操作返回,否則從根結點開始(注意並不是先訪問根結點),中序遍歷根結點的左子樹,然後是訪問根結點,最後中序遍歷右子樹。
如下圖,遍歷的順序爲:GDHBAEICF
接下來我們以下圖爲例對中序遍歷過程做以分析:
遍歷步驟:
- 根節點A存在左子樹,先中序遍歷以節點B爲根節點的左子樹
- 節點B存在左子樹,中序遍歷以節點D爲根節點的左子樹
- 節點D存在左葉子節點H,此時結果爲HD
- 節點D存在右葉子節點I,此時結果爲HDI
- 以節點B爲根節點的左子樹遍歷完成,此時訪問根節點B,結果爲HDIB
- 訪問B節點的右子樹,右子樹根節點存在左子樹J,此時結果爲HDIBJE
- 根節點A的左子樹訪問完成,訪問根節點A,結果變爲HDIBJEA
- 遍歷根節點A的右子樹,右子樹存在左葉子節點F,此時結果爲HDIBJEAF
- 然後訪問右子樹的根節點C,中序遍歷最後結果爲HDIBJEAFCG
後序遍歷
規則是若樹爲空,則空操作返回,否則從左到右先葉子後結點的方式遍歷訪問左右子樹,最後是訪問根結點。
如下圖,遍歷的順序爲:GHDBIEFCA
接下來我們以下圖爲例對後序遍歷過程做以分析:
遍歷步驟:
- 根節點A存在左子樹,先後序遍歷以節點B爲根節點的左子樹
- 節點B存在左子樹,後序遍歷以節點D爲根節點的左子樹
- 節點D存在左葉子節點H,右葉子節點I,此時結果爲HID
- 節點B存在右子樹,後序遍歷以節點E爲根節點的右子樹
- 節點E存在左葉子節點,不存在右葉子節點,此時結果爲HIDJE
- B節點的左子樹、右子樹遍歷完成,訪問B節點,結果爲HIDJEB
- 根節點的左子樹遍歷完成,後序遍歷根節點的右子樹,結果爲HIDJEBFGC
- 最後訪問根節點,後序遍歷最後結果爲HIDJEBFGCA
層序遍歷
規則是若樹爲空,則空操作返回,否則從樹的第一層,也就是根結點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問。
如下圖,遍歷的順序爲:ABCDEFGHI
接下來我們以下圖爲例對層序遍歷過程做以分析:
遍歷步驟:
- 從第一層根節點開始訪問,結果爲A
- 訪問第二層,從左到右,結果爲ABC
- 訪問第三層,結果爲ABCDEFG
- 訪問第四層,層序遍歷結果爲ABCDEFGHIJ
前序遍歷算法
遞歸實現
/*
* 前序遍歷的遞歸實現,我們直接根據定義,首先先訪問根節點,
* 然後前序遍歷左子樹,接着前序遍歷右子樹。
* 上述的每個前序遍歷就是一個遞歸的過程。
*/
void preOrder()
{
preOrder(_root);
cout << endl;
}
void preOrder(BSTNode* node)
{
if (node != nullptr)
{
cout << node->_data << " ";
preOrder(node->_left);
preOrder(node->_right);
}
}
非遞歸實現
/*
* 前序遍歷的非遞歸實現,我們需要藉助棧這個數據結構,由於前序遍歷是
* 先訪問根節點,然後前序遍歷左子樹,接着前序遍歷右子樹。
* 那麼非遞歸實現的話,我們先壓入根節點,然後打印棧頂元素,但是由於
* 出棧和入棧的順序是相反的,因此我們接着需要先壓入右孩子,再壓入左孩子
* 然後打印棧頂元素,繼續壓入左孩子的右孩子,左孩子的左孩子···
* 上述便是迭代循環的過程。
*/
void nonpreOrder()
{
if (_root == nullptr)
{
return;
}
stack <BSTNode*> stack;
stack.push(_root);
while (!stack.empty())
{
BSTNode* top = stack.top();
cout << top->_data << " ";
stack.pop();
if (top->_right != nullptr)
{
stack.push(top->_right);
}
if (top->_left != nullptr)
{
stack.push(top->_left);
}
}
cout << endl;
}
中序遍歷算法
遞歸實現
/*
* 中序遍歷的遞歸實現,根據定義,我們首先中序遍歷左子樹,
* 再打印根節點,然後再中序遍歷右子樹。
* 上述的每個中序遍歷就是一個遞歸的過程。
*/
void inOrder()
{
inOrder(_root);
cout << endl;
}
void inOrder(BSTNode* node)
{
if (node != nullptr)
{
inOrder(node->_left);
cout << node->_data << " ";
inOrder(node->_right);
}
}
非遞歸實現
/*
* 中序遍歷的非遞歸實現,我們首先應該一直向左遍歷,將結點壓棧,
* 直到結點爲空,然後我們打印該節點值,並將其出棧,並繼續遍歷
* 該節點的右子樹,繼續上述過程。
*/
void noninOrder()
{
if (_root == nullptr)
{
return;
}
stack<BSTNode*> stack;
BSTNode* top = _root;
while (!stack.empty() || top != nullptr)
{
if (top != nullptr)
{
stack.push(top);
top = top->_left;
}
else
{
top = stack.top();
cout << top->_data << " ";
stack.pop();
top = top->_right;
}
}
cout << endl;
}
後序遍歷算法
遞歸實現
/*
* 後序遍歷的遞歸實現,我們只需先後序遍歷左子樹,再後序遍歷右子樹
* 最後打印根節點值即可。
* 上述的每個後序遍歷就是一個遞歸的過程。
*/
void lastOrder()
{
lastOrder(_root);
cout << endl;
}
void lastOrder(BSTNode* node)
{
if (node != nullptr)
{
lastOrder(node->_left);
lastOrder(node->_right);
cout << node->_data << " ";
}
}
非遞歸實現
/*
* 後序遍歷的非遞歸實現,由於其根節點最後打印,因此我們需要藉助兩個棧來完成,
* 因爲我們需要藉助一個輔助棧來保存其父節點,而另一個棧則保存我們的結果集。
* 注意壓棧順序,本來我們應該先壓右孩子,再壓左孩子。但是我們使用另一個結果棧
* 來存儲結果集,輔助棧中的元素最終是出棧並壓入結果棧的,因此,我們程序中
* 應該先壓入左孩子,再壓入右孩子。
* 然後每次循環將棧頂元素出棧並壓入結果棧中。
* 最後,結果棧中所存儲的就是我們所需的後序遍歷結果集。我們打印出棧中所有的
* 元素即可
*/
void nonlastOrder()
{
if (_root == nullptr)
{
return;
}
stack<BSTNode*> Stack;
stack<BSTNode*> StackRes;
BSTNode* top = _root;
Stack.push(top);
while (!Stack.empty())
{
top = Stack.top();
StackRes.push(top);
Stack.pop();
if (top->_left != nullptr)
{
Stack.push(top->_left);
}
if (top->_right != nullptr)
{
Stack.push(top->_right);
}
}
while (!StackRes.empty())
{
top = StackRes.top();
cout << top->_data << " ";
StackRes.pop();
}
cout << endl;
}
層序遍歷算法
遞歸實現
/*
* 層序遍歷不同於之前講解的前序、中序、後序遍歷的深度優先遍歷,而它是一種典型的
* 廣度優先遍歷算法,那麼對於層序遍歷的遞歸實現,我們必須要獲取到該樹的層數,
* 才能對遞歸進行一個有效的控制
* 整體思路就是,我們在API接口中,向遞歸函數循環傳入層數(0-n)
* 在遞歸函數中,層數爲零,我們直接打印其值即可,若層數不爲零,我們就遞歸
* 地繼續向下遍歷。
* 如何向下繼續遍歷?我們每次遞歸遍歷就將層數減一即可,那麼比如我們遍歷第3層
* 元素,那麼傳入的參數爲3,我們遞歸調用函數,不斷的將參數減一,當減到0時便是
* 已經遍歷到第三層了,參數減到0,遞歸結束條件滿足,我們直接打印其值即可。
*/
void levelOrder()
{
int level = Treelevel(); // 求層數
for (int i = 0; i < level; ++i)
{
levelOrder(_root, i);
}
}
void levelOrder(BSTNode* node, int level)
{
if (node == nullptr)
{
return;
}
if (level == 0)
{
cout << node->_data << " ";
}
else
{
levelOrder(node->_left, level - 1);
levelOrder(node->_right, level - 1);
}
}
int Treelevel(BSTNode* node)
{
if (node == nullptr)
{
return 0;
}
int left = Treelevel(node->_left);
int right = Treelevel(node->_right);
return (left > right ? left : right) + 1;
}
非遞歸實現
/*
* 層序遍歷的非遞歸實現,正常思路我們先遍歷左子樹,再遍歷右子樹,由於是
* 層序遍歷,從上到下,從左到右,那麼從左到右剛好滿足隊列的性質,先入先出
* 那麼我們需要藉助一個隊列來保存我們遍歷到的元素。
* 首先壓入根節點。
* 每次遍歷我們拿到隊列的首元素,然後遍歷其左孩子,併入隊;然後遍歷其右孩子
* 併入隊,然後將隊首元素打印並出隊,繼續迭代直到隊列爲空。
*/
void nonlevelOrder()
{
queue<BSTNode *> Nodequeue;
if (_root == nullptr)
{
return;
}
Nodequeue.push(_root);
while (!Nodequeue.empty())
{
BSTNode* front = Nodequeue.front();
if (front->_left != nullptr)
{
Nodequeue.push(front->_left);
}
if (front->_right != nullptr)
{
Nodequeue.push(front->_right);
}
cout << front->_data << " ";
Nodequeue.pop();
}
cout << endl;
}