二叉樹的遍歷

2021-08-20 03:21:53 字數 3979 閱讀 6663

在二叉樹的一些應用中,常常要求在樹中查詢某種特徵的結點,or對樹中全部結點逐一進行操作。這就提出了遍歷二叉樹的問題,即如何按某條搜尋路徑巡訪樹中每個結點,使得每個結點都被訪問一次,而且僅訪問一次。

遍歷對於二叉樹來說,並不容易,因為二叉樹是一種非線性結構,每個結點都可能有兩顆子樹,因而需要尋找一種順序使每個結點排列在乙個線性佇列上,從而遍歷,即化非線性為線性。

對於二叉樹來說,二叉樹由3個基本單元組成:根結點,左子樹,右子樹。因此,遍歷完這3個部分就遍歷完整個二叉樹。遍歷這3個部分又有6個方案,即根左右、根右左、左右根、左根右、右左根、右根左。規定先左後右的話就有3種情況,即根左右、左根右、左右根(當然也可以先右後左啦),這3種情況就分別被稱為先序、中序、後序遍歷。看到先序、中序、後序有沒有點眼熟?之前的算數表示式的字首、中綴、字尾有什麼關係?這我在後面會解決這個問題的。

#遞迴演算法

##先序遞迴演算法

思想:若二叉樹為空樹,空操作;否則

訪問根結點;

先序遍歷左子樹;

先序遍歷右子樹。

演算法**:

void preorder (bitree t,status (

* visit)

(telemtype e))

}

思想:若二叉樹為空樹,空操作;否則

中序遍歷左子樹;

訪問根結點;

中序訪問右子樹。

演算法**:

void inorder (bitree t,status (

* visit)

(telemtype e))

}

思想:若二叉樹為空樹,空操作;否則

中序遍歷左子樹;

中序訪問右子樹;

訪問根結點。

演算法**:

void inorder (bitree t,status (

* visit)

(telemtype e))

}

首先有了遞迴演算法,**簡單又容易理解,為什麼還要學習非遞迴演算法?因為非遞迴演算法比遞迴演算法效率更高。

對於這樣一棵二叉樹來說,我們由下面這種路徑來遍歷,在這個過程中,由第幾次遇到根結點輸出來區分先序、中序、後序

在上面的過程中第一次遇到根結點就輸出根結點,是先序;第二次遇到根結點再輸出的是中序;第三次遇到才輸出的是後序。

我們注意到,在上面的過程中有返回雙親結點的路徑,因此我們需要保留之前走過的路徑,需要用到棧的結構方便回溯。

棧頂元素左子樹非空,且沒有遍歷過左右子樹,讓左孩子入棧遍歷左子樹

從左子樹返回(左孩子出棧),讓右孩子入棧,遍歷右子樹

從右子樹返回(右孩子出棧),根結點出棧。

我們發現,遍歷右子樹的時候,不需要儲存根結點。遍歷完左子樹需要根結點的路徑是因為還有右子樹沒有開始遍歷,需要根結點來提供乙個前往右子樹的「路徑」(指標),但是遍歷右子樹之後,根結點左右子樹就遍歷了,也就不需要在儲存根結點這個路徑了

void preorder(bitree t,status (* visit)(telemtype e))

else}}

中序演算法和先序演算法很像,只是輸出根結點的時機不同

void preorder(bitree t,status (* visit)(telemtype e))

else}}

後序和先序、中序相比就麻煩一些。先序中序在遍歷右子樹的時候,並不需要儲存根結點了,因為遍歷完右子樹就完成工作了,不需要在找到根結點了,因為根結點已經輸出過了。

但是後序不同,後序先遍歷左子樹,再遍歷右子樹,最後是根結點。這就需要我們遍歷完左子樹,根據根結點再去遍歷右子樹,遍歷完右子樹還要能找到根結點。

這就帶了乙個問題,從左子樹需要返回到根結點,然後再去遍歷右子樹;從右子樹也要返回根結點,再去訪問根結點。經過了根結點多次,要如何在演算法中判定根結點什麼時候輸出,什麼時候遍歷子樹(甚至是遍歷那棵子樹)?

在這種方法中,會給大家介紹三種演算法,但是只會詳細講解其中的兩種演算法。

演算法思想:

通過pre指標來判斷是否遍歷完cur的左右子樹,

如果遍歷完就輸出cur;

否則,如果左子樹沒遍歷就遍歷左子樹;

如果右子樹沒遍歷就遍歷右子樹。

status last (bitnode* t)

else

else}}

return ok;

}

這裡其實有使用到後序遍歷的乙個性質,如果根結點輸出,表示這棵樹遍歷完成。在上面的演算法中pre指向前一次出棧元素,也就說如果pre非空,那麼以pre指向的結點為根結點的子樹是遍歷完成的。

在上面出棧條件中,有要求到pre非空,這是從pre的含義上來理解的,條件是從右子樹返回or從左子樹返回並且右子樹不存在,這就隱含了乙個條件,一定有乙個子樹遍歷完了,根據上面的性質,子樹遍歷完,pre指向它的根結點,所有pre為空是要排除的

演算法思想:

後序遍歷順序lrd,入棧的順序就會是drl,所以每次入棧先讓右子樹入棧,

再讓左子樹入棧。如果棧頂元素cur是上一次出棧元素pre的雙親結點,就讓cur

出棧,否則,讓cur的左右子樹根結點入棧。

status last

(bitnode * t)

else

}}

演算法思想:

在上面演算法二中,我們提到了後序遍歷順序的乙個逆序入棧,由lrd變為drl,其實我們可以直接將drl以先遍歷右子樹再遍歷左子樹的先序遍歷演算法實現,然後將輸出的序列逆序就ok了。這個比較容易實現,具體演算法就不給出了。

###使用標誌域來判定

演算法思想:

所謂使用標誌域來判定,就是新增乙個標誌域來記錄第幾次遇到根結點。

如果是第三次遇到,就輸出根結點;

如果是第二次遇到,就遍歷右子樹;

如果是第一次遇到,就遍歷左子樹。

其實在一開始遇到後序非遞迴演算法的時候,我有考慮這個方法,因為這是根據上面的遍歷過程中路徑來做的,每個結點都會遇到3次,先序、中序、後序的區別只是在第幾次遇到這個結點輸出的差別,刨除掉這個訪問的過程,先序、中序、後序在遍歷過程中是一樣的(這就是最後的一部分給出的統一演算法思想了)。

那麼我為什麼沒有繼續做下去呢?因為我考慮到,需要乙個標誌域的話,就要改變我們二叉樹的儲存結構了,需要新增乙個int型的flag變數,在遍歷中倒是無所謂,比較需要flag的輔助,可如果給你一棵樹,它的操作大部分都不是遍歷的時候,新增的這個flag變數就顯得很雞肋,降低的儲存的密度。

既然降低了儲存的密度,我們又可以用上面的方法完成,我們又為什麼要學這種方法呢?這種方法有什麼好處呢?這種方法很完美的體現了上面路徑圖中的每個結點訪問3次的情況。像我們上面的方法中,葉子結點只訪問1次,單側子樹的結點只訪問2次,只有兩側都有子樹的結點才訪問3次。這就不便於我們寫出一種統一的演算法來完成遍歷操作。

如果只是為了乙個統一的演算法,犧牲儲存密度好像說不出去吧,沒有一種方法能夠即可以完成統一演算法又可以不改變二叉樹結構嗎?

當然有了,如果是在二叉樹結點中新增標誌域,我是很不提倡的,在我們的演算法中,由於需要回溯,因此需要使用棧,我們可以在棧中新增標誌域,這樣就可以達到使用遍歷的時候會出現標誌域,不進行遍歷的話就不會出現標誌域。

棧的結構:

typedef stack

具體演算法我這裡就不寫了,直接在下面給出乙個統一演算法了。

演算法思想在使用標誌域判定的方法中已經介紹過了,就不再重述了,直接上演算法。

void allorder (bitree* t,

int order)

case2:

case3:

}}else

}}

二叉樹的遍歷 二叉樹遍歷與儲存

在資料結構中,二叉樹是非常重要的結構。例如 資料庫中經常用到b 樹結構。那麼資料庫是如何去單個查詢或者範圍查詢?首先得理解二叉樹的幾種遍歷順序 先序 中序 後序 層次遍歷。先序 根節點 左子樹 右子樹 中序 左子樹 根節點 右子樹 後序 左子樹 右子樹 根節點 按層級 class node if c...

構建二叉樹 遍歷二叉樹

陣列法構建二叉樹 public class main public static void main string args 用陣列的方式構建二叉樹 public static void createbintree 把linkedlist集合轉成二叉樹的形式 for int j 0 j 最後乙個父節...

玩轉二叉樹(二叉樹的遍歷)

時間限制 400 ms 記憶體限制 65536 kb 長度限制 8000 b 判題程式 standard 作者 陳越 給定一棵二叉樹的中序遍歷和前序遍歷,請你先將樹做個鏡面反轉,再輸出反轉後的層序遍歷的序列。所謂鏡面反轉,是指將所有非葉結點的左右孩子對換。這裡假設鍵值都是互不相等的正整數。輸入格式 ...