一、前言
《二叉查找樹全面詳細介紹》中講解了二叉樹操作:搜索(查找)、遍歷、插入、刪除。其中遍歷深度優先遍歷(DFS)按照實現方法可以分爲:遞歸遍歷實現、非遞歸遍歷實現、Morris遍歷實現,文中只給了代碼,沒有對實現過程進行講解,本文將對遞歸遍歷實現、非遞歸遍歷棧實現、Morris遍歷實現這三類實現方法進行講解。
二、三類實現方法特點
遞歸實現
- 編碼簡單,易於理解;
- 隱式使用棧存儲當前沒有處理完的節點信息;
- 若樹深度大,遞歸深度大可能會超過堆棧保留大小;
時間複雜度:O(n),空間複雜度:O(logn)
非遞歸棧實現
- 顯示使用棧存儲當前沒有處理完的節點信息,需要花費額外時間來維護棧併爲棧留出空間;
- 若樹結構不是理想結構(呈線性)棧可能需要保存樹中幾乎所有節點信息,當樹很大的時候這會是一個嚴重問題;
- 效率相對遞歸實現會高一些,實現起來會比遞歸實現複雜一些;
時間複雜度:O(n),空間複雜度:O(logn)
Morris遍歷
- 佔用空間少,不用額外花費時間維護棧和爲棧留出空間;
- 利用了樹中空節點;
- 實現複雜,理解有一定難度;
時間複雜度:O(n),空間複雜度:O(1)
三、遞歸遍歷實現
遞歸深度遍歷是最常見的寫法,也最好理解,直接看代碼。
// 前序遍歷,遞歸實現
template<class T>
void BST<T>::preorder(BSTNode<T>* p)
{
if (p != 0)
{
visit(p);
preorder(p->m_left);
preorder(p->m_right);
}
}
// 中序遍歷,遞歸實現
template<class T>
void BST<T>::inorder(BSTNode<T>* p)
{
if (p != 0)
{
inorder(p->m_left);
visit(p);
inorder(p->m_right);
}
}
// 後序遍歷,遞歸實現
template<class T>
void BST<T>::postorder(BSTNode<T>* p)
{
if (p != 0)
{
postorder(p->m_left);
postorder(p->m_right);
visit(p);
}
}
四、非遞歸遍歷實現
4.1 前序遍歷,非遞歸棧實現
4.1.1 實現步驟
- 起始將樹根節點添加到棧中;
- 棧彈出一個元素,訪問該節點,並將該節點的右子節點和左子節點添加到棧中。說明,節點爲空不用添加;
- 判斷棧是否爲空,爲空樹遍歷結束,不爲空繼續步驟2。
4.1.2 實例演示
棧的變化過程
4.1.3 編碼實現
// 前序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativePreorder()
{
Stack<BSTNode<T>*> travStack;
BSTNode<T>* p = root;
if (p != 0)
{
travStack.push(p);
while (!travStack.empty())
{
p = travStack.pop();
visit(p);
if (p->m_right != 0)
travStack.push(p->m_right);
if (p->m_left != 0)
travStack.push(p->m_left);
}
}
}
4.2 中序遍歷,非遞歸棧實現
4.2.1 實現步驟
中序遍歷,非遞歸棧實現,提供了兩種方法,思路差不多,做了一些改動,方法一iterativeInorder是《C++數據結構與算法》中提供的實現方法;方法二iterativeInorder_2,做了一些改動,思想是一樣的,個人認爲方法二更好一點點,這裏實現步驟和思路演示用的是方法二。
- 將樹根節點賦值給變量p;
- 循環判斷p是否爲空,非空執行循環;
- 循環中將p節點添加到棧中,訪問p節點的左節點,非空就添加到棧中,繼續訪問p節點左節點的左節點非空就添加到棧中,直到左節點爲空;
- 彈出棧頂元素將其賦值給p,並訪問該元素;
- 棧不爲空且彈出的棧頂元素沒有右節點,繼續彈出棧元素並訪問它直到,棧爲空或者彈出的節點有右節點;
- 將最後一個彈出的節點右節點賦值給p,回到步驟2,繼續執行;
4.2.2 實例演示
棧的變化過程
4.2.3 編碼實現
// 中序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativeInorder()
{
Stack<BSTNode<T>*> travStack;
BSTNode<T>* p = root;
while (p != nullptr)
{
while (p != nullptr)
{
if (p->m_right)
travStack.push(p->m_right);
travStack.push(p);
p = p->m_left;
}
p = travStack.pop();
while (!travStack.empty() && p->m_right == nullptr)
{
visit(p);
p = travStack.pop();
}
visit(p);
if (!travStack.empty())
p = travStack.pop();
else
p = nullptr;
}
}
// 中序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativeInorder_2()
{
Stack<BSTNode<T>*> travStack;
BSTNode<T>* p = root;
while (p != nullptr)
{
travStack.push(p);
for (; p != nullptr && p->m_left != nullptr; p = p->m_left)
travStack.push(p->m_left);
p = travStack.pop();
visit(p);
while (!travStack.empty() && p->m_right == nullptr)
{
p = travStack.pop();
visit(p);
}
p = p->m_right;
}
}
4.3 後序遍歷,非遞歸棧實現
和中序遍歷非遞歸棧實現有些類似,不同的是:
中序遍歷彈出左節點之後,繼續彈出父節點,然後判斷父節點是否有右節點,沒有繼續彈出下一個節點;有則以這個右節點爲節點進行下一次循環遍歷。
後序遍歷彈出左節點之後,繼續彈出父節點,然後判斷父節點是否有右節點,沒有繼續彈出下一個節點;有右節點且這個右節點之前已經訪問過,繼續彈出下一個節點;有右節點且這個右節點之前沒有訪問過,則以這個右節點爲節點進行下一次循環遍歷。
最主要的是需要理解當一個節點的右節點已經被訪問了,則它的右節點不需要再被訪問。
4.3.1 實現步驟
- 將樹根節點賦值給變量p,定義一個標記變量guard將根節點賦值給它;
- 循環判斷p是否爲空,非空執行循環;
- 循環中將p節點添加到棧中,訪問p節點的左節點,非空就添加到棧中,繼續訪問p節點左節點的左節點非空就添加到棧中,直到左節點爲空;
- 將p賦值給guard,彈出棧頂元素將其賦值給p,並訪問該元素;
- 棧不爲空且彈出的棧頂元素沒有右節點或者有右節點但該右節點和guard相等(也就是右節點剛剛被訪問過),繼續彈出棧元素並訪問它直到,棧爲空或者彈出的節點有右節點且沒有被訪問;
- 將最後一個彈出的節點右節點賦值給p,回到步驟2,繼續執行
4.3.2 實例演示
棧的變化過程
說明:在棧變化過程中的第四個棧,棧頂是D,在對D節點右訪問的時候,發現節點I已經被訪問過,此時不需要再訪問D的右節點,直接輸出D節點並彈出D節點即可。
4.3.3 編碼實現
// 後序遍歷,非遞歸棧實現
template<class T>
void BST<T>::iterativePostorder()
{
Stack<BSTNode<T>*> travStack;
BSTNode<T>* p = root, * guard = root;
while (p != nullptr)
{
for (; p->m_left != nullptr; p = p->m_left)
travStack.push(p);
while (p->m_right == nullptr || p->m_right == guard)
{
visit(p);
if (travStack.empty())
return;
guard = p;
p = travStack.pop();
}
travStack.push(p);
p = p->m_right;
}
}
五、Morris遍歷
利用空閒節點找到回去的路,和避免走重複的路。
5.1 前序遍歷,Morris遍歷算法實現
5.1.1 實現步驟
- 將樹根節點賦值給變量p;
- 若p節點沒有左節點,輸出p節點,並將p指向它的右節點
- 若p節點有左節點,找到p的左節點中最後訪問節點(temp)
- 若temp節點右節點爲空,temp的右節點指向p節點,輸出p節點,並將p指向它的左節點
- 若temp節點右節點不爲空,temp的右節點設置爲空(恢復原始狀態),輸出p節點,並將p指向它的右節點
- 會到步驟2
5.1.2 實例演示
1) 樹結構如下:
2)p指向節點A,A左節點中最後訪問節點是E,將E的指向A,輸出節點A。p指向A左節點B。
3)p指向節點B,B左節點中最後訪問節點是D,將D的指向B,輸出節點B。p指向B左節點D。
4)p指向節點D,D沒左節點,輸出節點D,p指向D右節點B。
5)p指向節點B,B左節點中最後訪問節點是D,由於D的右節點指向B,說明D節點已經被訪問了,不需要重複訪問,恢復指針狀態,將D的右指針設置爲空。p指向B的右節點E。
6)p指向節點E,E沒有左節點,輸出節點E,p指向E的右節點A。
說明:這裏E沒有左右節點,如果有左右節點處理過程和A節點是一樣的。
7)p指向節點A,A左節點中最後訪問節點是E,由於E的右節點指向A,說明A節點已經被訪問了,不需要重複訪問,恢復指針狀態,將E的右指針設置爲空。p指向A的右節點C。過程同5。
8)p指向節點C,C有左節點,輸出節點C,p指向C的右節點,p爲空遍歷結束。
5.1.3 編碼實現
// 前序遍歷,非遞歸Morris遍歷算法實現
template<class T>
void BST<T>::MorrisPreorder()
{
BSTNode<T>* p = root, * tmp;
while (p != nullptr)
{
if (p->m_left == nullptr)
{
visit(p);
p = p->m_right;
}
else
{
tmp = p->m_left;
while (tmp->m_right != nullptr && tmp->m_right != p)
tmp = tmp->m_right;
if (tmp->m_right == nullptr)
{
visit(p);
tmp->m_right = p;
p = p->m_left;
}
else
{
tmp->m_right = nullptr;
p = p->m_right;
}
}
}
}
5.2 中序遍歷,Morris遍歷算法實現
5.2.1 實現步驟
和前序遍歷,Morris遍歷算法類似,只是輸出節點順序有一些變動。直接看代碼
5.2.2 實例演示
演示的圖和前序是一樣的,只是輸出的節點順序不一樣,如下圖紅色點爲輸出的節點。
5.2.3 編碼實現
// 中序遍歷,非遞歸Morris遍歷算法實現
template<class T>
void BST<T>::MorrisInorder()
{
BSTNode<T>* p = root, * tmp;
while (p != 0)
{
if (p->m_left == nullptr)
{
visit(p);
p = p->m_right;
}
else
{
tmp = p->m_left;
while (tmp->m_right != 0 && tmp->m_right != p)
tmp = tmp->m_right;
if (tmp->m_right == nullptr)
{
tmp->m_right = p;
p = p->m_left;
}
else
{
visit(p);
tmp->m_right = nullptr;
p = p->m_right;
}
}
}
}
5.3 中序遍歷,Morris遍歷算法實現
5.3.1 實現步驟
直接看演示圖和代碼吧,有點複雜,需要細看,圖細解!
5.3.2 實例演示
約定:對於有左節點的父節點,我們需要找到父節點的左節點,並沿着這個左節點一直找它的右節點直到葉子節點爲止,然後將右節點指向起始的父節點,這個最右節點查找等價於,查找一顆二叉搜索左節點中最大的一個節點。有點繞,後面需要用到,直接看下面圖解就很容易理解。
1)待遍歷的樹
2)新加一個節點,它的左節點指向樹的根節點。新加節點是爲了能在遍歷的規則下輸出樹根節點。
3)p指向節點R,p的左節點中最大的節點是A,將A的右節點指向R,p指向R節點的左節點A。
4)p指向節點A,A的左節點中最大的節點是H,將H的右節點指向A,p指向A節點的左節點B。
5)p指向節點B,B的左節點中最大的節點是C,將C的右節點指向B,p指向B節點的左節點C。
6) p指向節點C,C沒有左節點,p指向C節點的右節點B。
7) p指向節點B,B節點的左節點是C,C右節點指向了B自己,輸出節點C,p指向B節點的右節點D
8)p指向節點D,D沒有左節點,P指向D的右節點E。
9)p指向節點E,E的左節點中最大的節點是F,將F的右節點指向E,p指向E節點的左節點F。
10)p指向節點F,F沒有左節點,p指向F節點的右節點E。
11) p指向節點E,E節點的左節點是F,F右節點指向了E自己,輸出節點F,p指向E節點的右節點G
12)p指向節點G,G沒有左節點,P指向G的右節點H。
13)p指向節點H,H的左節點中最大的節點是I,將I的右節點指向H,p指向H節點的左節點I。
14)p指向節點I,I沒有左節點,p指向I節點的右節點H。
15)p指向節點H,H節點的左節點是I,I右節點指向了H自己,輸出節點I,p指向H節點的右節點A。
16)p指向節點A,p左節點最大節點是A,指向了自己,將A節點的右節點指向順序進行翻轉。在翻轉過程中,q指向最後一個右節點,R指向了起始節點A,對S指針賦值爲H的右節點G,通過這些條件,我們可以從下往上遍歷起始的節點左節點的右節點。
輸出節點的順序是H、G、E、D、B,並恢復指針指向狀態。p指向A節點的右節點R。
17)p指向節點R,R節點的左節點是A,A右節點指向了R自己,輸出節點A。遍歷完成。
總結一下:
最主要的是需要理解步驟16,先進行一次翻轉,然後輸出節點恢復指針狀態。
5.3.3 編碼實現
// 後序遍歷,非遞歸Morris遍歷算法實現
template<class T>
void BST<T>::MorrisPostorder()
{
BSTNode<T>* p = new BSTNode<T>(), * tmp, * q, * r, * s;
p->m_left = root;
while (p != 0)
{
if (p->m_left == nullptr)
p = p->m_right;
else
{
tmp = p->m_left;
while (tmp->m_right != nullptr && tmp->m_right != p)
tmp = tmp->m_right;
if (tmp->m_right == nullptr)
{
tmp->m_right = p;
p = p->m_left;
}
else
{
for (q = p->m_left, r = q->m_right, s = r->m_right;
r != p; q = r, r = s, s = s->m_right)
r->m_right = q;
for (s = q->m_right; q != p->m_left;
q->m_right = r, r = q, q = s, s = s->m_right)
visit(q);
visit(p->m_left);
tmp->m_right = nullptr;
p = p->m_right;
}
}
}
}