基本數據結構學習總結: 二叉樹的遍歷
二叉搜索樹的遍歷
二叉樹遍歷的內容很多,但是也是最重要的,最需要理解的,很多二叉樹的相關算法,只要用活了遍歷就沒有問題了
前序遍歷
對於每一棵樹,先遍歷其根節點,然後遍歷其左子樹,最後用同樣的方式遍歷右子樹
遞歸實現前序遍歷的過程太簡單了,這裏就不放了,直接說明二叉樹的非遞歸的前序遍歷實現:
如果不用遞歸實現前序遍歷,那麽就必須要用棧了,棧的作用是什麽呢?我覺得棧就是記錄了你的遍歷過程,而且可以告訴你下一次應該回溯到哪個節點,每一次取出棧頂的元素,都說明已經遍歷完這個棧頂節點的左子樹了。
思路也很簡單,就是每一次遇到一個節點就放入棧中,因為是前序遍歷,所以先輸出根節點,然後遍歷左子樹,當發現左子樹遍歷完成後,根據前序遍歷的規則,此時需要遍歷右子樹,所以就取出棧頂節點,並遍歷棧頂節點的右子樹
bTree* stack[50] = {0};
int top = 0;
bTree *now = root;
while(!isNull(now) || top)
{
while(!isNull(now))
{
stack[top++] = now;
std::cout << now->key << std::endl;
now = now->left;
}
now = stack[--top];
now = now->right;
}
中序遍歷
對於每一棵樹,先遍歷其左子樹,然後遍歷其根節點,最後用同樣的方式遍歷右子樹
- 方法1:使用棧:
思路和前序遍歷差不多,但是這一次因為是中序遍歷,所以每一次取出棧頂節點時,說明左子樹遍歷完畢,那麽此時輸出中間節點,並遍歷右子樹,用棧很容易實現,但是這裏可以不用棧(這也是算法導論的一道題)
void inorderWalk() { bTree* stack[50] = {0}; int top = 0; bTree *now = root; while(!isNull(now) || top) { while(!isNull(now)) { stack[top++] = now; now = now->left; } now = stack[--top]; std::cout << now->key << std::endl; now = now->right; } }
- 方法2,使用一個變量記錄當前節點的父親節點
last就承擔了stack的責任,stack其實有用的也是棧頂元素,這裏last承擔了這個棧頂元素的責任
void inorderWalkNotUseStack()
{
bTree *last = 0;
bTree *now = root;
while(!isNull(now))
{
//last永遠記錄now的父親,如果now非null,那麽父親也可以通過now->p來得到,但是當now是null時就需要通過last得到
if(!isNull(now))
{
//如果now不是null
last = now;
now = now->left;
}
else{
//此時說明now是null,那麽now可能是父親的左子節點,也可能是右子節點
//如果now是某個節點的左子節點,那麽說明此時應輸出now的父親,然後設置now為父親的right
//如果now是某個節點的右子節點,而且now還是null,那麽借助last回溯,完全可以根據last和last的父親的關系,把now設置為0或者1(不能直接設置為last,否則會重復遍歷,其實這裏就相當於把last所在的子樹設置為null!這樣就相當於last所在子樹已經被遍歷完畢)
//為了更好的區分左右節點,我把原來為null的左子節點值設置為0,右子節點值設置為1,這樣可以區分是左子節點還是右子節點,如果左右都是null就區分不了是左還是右子節點了
if(now == (bTree *)1)
{
//說明now是右子節點,那麽此時看last即它的父親是否為root,如果是那麽說明遍歷完成,如果不是,那麽照之前的設置即可
if(last == root)
{
break;
}
else{
//last不是root,那麽回溯即可
now = last == last->p->left ? 0 : (bTree*) 1;
last = last->p;
}
}
else{
std::cout << last->key << std::endl;
now = last->right;
}
}
}
}
- 方法3:其實中序遍歷還有個方法,就是利用中序遍歷的結果其實就是排好序的結果,所以我們可以通過先找到最小的節點,然後不斷的調用successor()方法不斷獲得其後繼。
//使用minimum函數和successor完成的中序遍歷
void inorderWalk2()
{
bTree *min = iterativeMinimum(root);
while(!isNull(min))
{
std::cout << min->key << std::endl;
min = successor(min);
}
}
後序遍歷
對於每一棵樹,先遍歷其左子樹,再遍歷右子樹,最後才遍歷根節點
後序遍歷比前兩種遍歷方式的麻煩之處在於,後序遍歷會兩次回溯根節點,因為在遍歷完左子樹以後,接下來要遍歷右子樹,而獲得右子樹必須要通過根節點獲得,這是第一次使用根節點,然後右子樹遍歷完以後,需要遍歷根節點了,此時第二次使用根節點。
所以必須要通過某種方式得知當前是第幾次遍歷這個根節點,有兩種方式:
- 第一種是通過設置一個標誌位棧和節點棧同步維護
- 另一種方式是我選擇的方式,就是第一次遍歷一個根節點,一定是從它的左子節點回溯的,第二次,一定是從它的右子節點回溯的,那麽如果我能得到當前節點是棧頂節點的左子節點還是右子節點來判斷是第幾次回溯了。
- 實現的方法就是給二叉樹節點增加p屬性,便於尋找其父節點。但光是增加p屬性還不行,對於節點是null,無法通過p屬性獲得其父親,而且當左右子節點都是null時,無法判斷當前節點是父親的左子節點還是右子節點,所以我設定:左子節點是null時,設置為0,右子節點是null時,設置為1,這樣就能區分開了
void postorderWalk()
{
bTree *stack[50] = {0};
int top = 0;
bTree *now = root;
while(!isNull(now) || top)
{
while(!isNull(now))
{
stack[top++] = now;
now = now->left;
}
//後序遍歷不能直接從棧中取節點,因為後序是先左再右,所以要先找右結點
//所以後序遍歷會取兩次節點,那麽什麽時候輸出這個節點呢,就要看當前節點和棧頂節點的關系了
//如果當前節點是棧頂節點的左子節點,那麽直接找右子節點,
//錯誤的想法:
//如果當前節點是棧頂結點的右子節點,那麽輸出並設置now為0,因為剛取出來的棧頂結點一定是已經遍歷完左子節點的
//這樣會導致死循環,正確的做法應該是不斷地取出棧頂元素,如果說當前結點是棧頂元素的右子節點,那麽繼續回溯
bTree *p = stack[top - 1];
if(now == 0)
{
//說明now是棧頂結點的左子節點
now = p->right;
}
else{
//說明now是棧頂結點的右子節點
bTree *q = now;
while(top && q == p->right)
{
//這裏要不斷的取出已經遍歷完的子樹的根節點
top--;
std::cout << p->key << std::endl;
q = p;
p = stack[top - 1];
}
if(top == 0)
{
//如果top是0說明已經遍歷完成了
break;
}
else{
//此時p是棧頂元素,q是p的左子節點,此時設置now為p的右子節點
now = p->right;
}
}
}
}
總結
這三種裏我覺得中序和後序是最重要的,中序遍歷結果是排序的結果,後序遍歷的話特點就是左右子節點都遍歷完以後才會遍歷父節點,所以很多算法會用到後序遍歷
基本數據結構學習總結: 二叉樹的遍歷