二叉樹遍歷:前序,中序,後序,層序的遞迴以及非遞迴實現
樹,是一種在實際程式設計中經常遇到的資料結構,它的邏輯很簡單:除根節點之外每個節點都有且只有一個父節點,除葉子節點之外所有節點都有一個或多個子節點。我們說的二叉樹,就是指子節點最多2個的樹。
二叉樹中,最重要的操作就是遍歷。二叉樹的遍歷分為:
1.前序遍歷:先訪問根節點,再訪問左子節點,最後訪問右子節點。
2.中序遍歷:先訪問左子節點,再訪問根節點,最後訪問右子節點。
3.後序遍歷:先訪問左子節點,再訪問右子節點,最後訪問根節點。
4.層序遍歷:也就是我們說的“廣度優先”或者“寬度優先”遍歷。先訪問第一層節點,再訪問第二層...直到最後一層。每一層的訪問順序都是從左到右。
在本文中,將寫出二叉樹的前中後序遍歷的遞迴實現以及非遞迴實現,還有層序遍歷的實現。
首先,我們給出二叉樹的定義。
typedef struct BinaryTree
{
int _data;
struct BinaryTree * _lchild;
struct BinaryTree * _rchild;
}Tree,*pTree;
可以看到,一個樹中節點結構包括值域,左孩子節點和右孩子節點。葉子節點的左右孩子為空。
然後是三種遍歷的遞迴實現。
- 前序遍歷的遞迴實現
void PreOrderRecursion(pTree node) { if(node == nullptr) return; cout<<node->_data<<" "; PreOrderRecursion(node->_lchild); PreOrderRecursion(node->_rchild); }
從根節點開始,先打印出根節點的值,再遞迴左子樹,最後遞迴右子樹。
中序、後序遍歷的思想和前序一樣,只需要調整列印的順序即可,所以下面不再贅述。
void InOrderRecursion(pTree node) { if(node == nullptr) return; InOrderRecursion(node->_lchild); cout << node->_data<<" "; InOrderRecursion(node->_rchild); } void PostOrderRecursion(pTree node) { if(node == nullptr) return; PostOrderRecursion(node->_lchild); PostOrderRecursion(node->_rchild); cout << node->_data<<" "; }
接下來是三種遍歷的非遞迴實現。我們說到其實遞迴的本質就是棧,既然不使用遞迴,那麼我們就要用到棧。
其中,前序遍歷和中序遍歷的思想也十分相似,只是後序遍歷有些許不同。
此處前序遍歷的非遞迴有兩種實現,思想有些許不同。
前序遍歷的非遞迴實現方法1:
思想:使用棧,首先判斷輸入合法性。隨後將頭節點首先入棧 ,保證棧中開始至少有一個元素。使用一個while迴圈,只要棧還非空就一直進行。迴圈體中首先獲取棧頂節點,將其列印後直接出棧,並將其的右孩子和左孩子依次入棧(如果存在的話)。根據棧的FILO特性,最後入棧的左孩子將在下一輪中成為棧頂元素。這樣就能滿足前序遍歷的特點。
void PreOrderIteration1(pTree node)
{
if(node == nullptr) return;
stack<pTree> s;
pTree p = node;
s.push(p);
while(!s.empty())
{
p = s.top();
cout<<p->_data<<" ";
s.pop();
if(p->_rchild) s.push(p->_rchild);
if(p->_lchild) s.push(p->_lchild);
}
}
前序遍歷非遞迴實現方法2:
思想:迴圈開始,從棧頂節點(除第一次是根節點)開始不斷左探,入棧並列印,直到左孩子為空;然後指標指向棧頂元素的右孩子,開啟下一輪迴圈。
void PreOrderIteration2(pTree node)
{
stack<pTree> s;
pTree p = node;
while(!s.empty() || p != nullptr)
{
while(p != nullptr) //不斷左探的過程
{
cout<< p->_data << " ";
s.push(p);
p = p->_lchild;
}
if(!s.empty()) //最後訪問右孩子
{
p = s.top();
s.pop();
p = p->_rchild;
}
}
}
總地來說,還是第一種方法更好理解也便於記憶。
中序遍歷非遞迴實現:
思想:與前序遍歷非遞迴方法2的思想相同,只是不在不斷左探的時候列印,而是在出棧的時候列印。
void InOrderIteration(pTree node)
{
if (node == nullptr) return;
pTree p = node;
stack<pTree> s;
while(!s.empty() || p != nullptr)
{
while(p != nullptr)
{
s.push(p);
p = p->_lchild;
}
if(!s.empty())
{
p = s.top();
cout << p->_data <<" ";
s.pop();
p = p->_rchild;
}
}
}
後序遍歷非遞迴實現:
思想:相對於前序和中序,後續遍歷的實現就稍顯麻煩。我們要保證一個節點要在左孩子和右孩子之後才能訪問,那麼有下面三種情況:
1.它是葉子節點,沒有左右孩子。可以直接訪問。
2.它有左右孩子,但是左右孩子都已經被訪問過,也可以直接訪問該節點。
3.有左右孩子,且左右孩子沒有訪問過。此時要先入棧右孩子,再入棧左孩子。
為了確認一個節點的左右孩子是否被訪問過,我們就要定義一個pPre指標。訪問過一個節點後,就將pPre指向它,在下一輪判斷就可以通過p->lchild || p->rchild == pPre來作為子節點是否被訪問過的條件了。
void PostOrderIteration(pTree node)
{
pTree pCur = node; //用來儲存當前節點的指標
pTree pPre = nullptr; //儲存上一個訪問的節點的指標
stack<pTree> s;
s.push(pCur);
while(s.!empty())
{
pCur = s.top();
if((pCur->_lchild == nullptr && pCur->_rchild == nullptr) || //沒有左右孩子的情況
(pPre != nullptr && (pPre == pCur->_lchild || pPre == pCur->_rchild))) //左右孩子訪問過的情況
{
cout << pCur->_data << " ";
s.pop();
pPre = pCur;
}
else
{
if(pCur->_rchild != nullptr) s.push(pCur->_rchild);
if(pCur->_lchild != nullptr) s.push(pCur->_lchild);
}
}
}
最後是層序遍歷。和其他三種遍歷不同的是,層序遍歷使用佇列來實現。先進先出也很符合層序遍歷的特點。
void LevelOrder(pTree node)
{
pTree p = node;
if(node == nullptr) return;
queue<pTree> q;
for(q.push(p);q.size();q.pop()) //只要佇列不空就一直出隊
{
p = q.front();
cout << p->_data <<" ";
if(p->_lchild) q.push(p->_lchild);
if(p->_rchild) q.push(p->_rchild);
}
}