二叉樹的非遞迴遍歷的實現
1.前言:當然了,這也是複習。因為資料結構要考試了,把之前的演算法拿出來敲一遍。
2.二叉樹的遍歷是一個基於二叉樹很重要的操作,應用也很多。我們知道遞迴是樹的特性,所以樹上的所有演算法幾乎都是基於遞迴做的。但是在一些嵌入式裝置中,系統棧不允許很深的遞迴。這時候就要把遞迴的演算法轉換為非遞迴演算法,嗯,沒錯,應用場景看上去有點實際,但是真正促使我的還是考試,哈哈。
先給出一個遍歷的遞迴演算法把。
typedef struct BinaryTreeNode { int data; struct BinaryTreeNode *Left; struct BinaryTreeNode *Right; }Node,*node; //先序遍歷 void preOrderTraverse(Node* root) { if (root) { cout << root->data << ' '; preOrderTraverse(root->Left); preOrderTraverse(root->Right); } } //中序遍歷 void inOrderTraverse(Node* root) { if (root) { inOrderTraverse(root->Left); cout << root->data << ' '; inOrderTraverse(root->Right); } } //後序遍歷 void lastOrderTraverse(Node* root) { if (root) { lastOrderTraverse(root->Left); lastOrderTraverse(root->Right); cout << root->data << ' '; } }
其實用遞迴寫很簡單了,就是看訪問的語句在什麼地方嘍,前邊就是前序,中間就是中序,後邊就是後序。程式碼十分的簡介而且易懂。但是,上邊我們也說了因為老師考試,哦不對,是有些裝置不允許這樣的程式出現在它內部。所以我們就要把遞迴轉成非遞迴演算法。那麼我們就來看一下怎麼轉換唄。
3.前序的非遞迴實現。我們知道前序遍歷的順序是根-左-右。根據這個特點我們知道,在訪問完根節點之後,我們就訪問他的左子樹,然後是右子樹。一種BFS的思想,其實也不難,就簡單寫一下OK。
void preOrder(Node*root) { stack<node>st; st.push(root); Node*tem = NULL; while (!st.empty()) { tem = st.top(); st.pop(); if (tem != NULL) { cout << tem->data << " "; st.push(tem->Left); st.push(tem->Right); } } }
4.中序遍歷的非遞迴實現。中序的順序是左-根-右。我們的想法是首先要遞迴左子樹,把左子樹的節點全部都放在棧裡,想象一下這個過程,其實這個棧的內部存的就是二叉樹最左邊的那條路徑。訪問的時候,我們首先彈出棧頂,訪問,檢查這個節點有沒有右子樹,如果有,切換為右子樹做同樣的操作。那麼這個時候有人可能會有疑問:根節點哪去了?其實仔細想想,根節點相對於它的上一個元素不就是左子樹上的點嗎?所以這樣就會按照這個順序訪問完所有的節點。並且,需要注意的是,這個左右子樹是在交替的,看上去棧裡只有左子樹的根節點,其實整棵樹都在。或者我覺得,左右子樹是一開始我們就定義好的,其實他們是一樣的。下邊給出程式碼實現:
void inOrder(Node*root) { stack<node>st; node p = root; do { while (p != NULL) { st.push(p); p = p->Left; } p = st.top(); st.pop(); cout << p->data << " "; if (p->Right != NULL) { p = p->Right; } else { p = NULL; } } while (p!=NULL||!st.empty()); }
5.最後是後續遍歷的實現:這個是有些困難的,我覺得是這三個中最不好寫的。我們知道後序的順序是左-右-根。這樣其實會出現一個問題,就是當你訪問根節點的時候不知帶左右子樹是否已經被訪問過了。解決辦法有很多,有的是給節點加一個屬性,但是我在這裡使用額外O(1)的空間來完成這件事。我們還是要左子樹入棧,但是需要有一個儲存最新被棧彈出的節點。我們跟著程式跑一遍來解釋這個問題。首先我們把左節點全部入棧,當前彈出來的節點初始化為root,當我們完成這些之後,開始訪問節點。這個時候已經被到了二叉樹最左邊的點,第一個if不成立了。檢查第二個依舊不成立。進入else彈出這個節點,更新彈出的節點,訪問它。好了,這個時候我們已經完成了最左邊的葉子節點的訪問(雖然不一定是葉子節點,但是不影響)。第二輪繼續進入迴圈,p被更新到父節點,雖然p-left不再是NULL,但是p-left=last,所以第一個條件不成立,這個時候我們發現第二個條件成立了,所以把p的右節點壓進去訪問。last被更新為右節點,繼續進入迴圈,這個時候我們發現兩個條件都是不成立的,所以只能訪問根節點。這樣就完成了訪問。是不是很神奇,程式碼之道,讓人驚歎!
//非遞迴後續遍歷二叉樹
void lastOrder(node root)
{
assert(root);
stack<node>st;
st.push(root);
node last = root;
while (!st.empty())
{
node p = st.top();
if (p->Left != NULL && p->Left != last && p->Right != last)
{
st.push(p->Left);
}
else if (p->Right != NULL && p->Right != last && (p->Left == NULL || p->Left == last))
{
st.push(p->Right);
}
else
{
st.pop();
last = p;
cout << p->data << " ";
}
}
}