非遞迴遍歷二叉樹

2021-05-24 10:26:59 字數 4232 閱讀 3292

1.先序遍歷

從遞迴說起

void  preorder(tnode* root) }

遞迴演算法非常的簡單。先訪問跟節點,然後訪問左節點,再訪問右節點。如果不用遞迴,那該怎麼做呢?仔細看一下遞迴程式,就會發現,其實每次都是走樹的左分支(left),直到左子樹為空,然後開始從遞迴的最深處返回,然後開始恢復遞迴現場,訪問右子樹。

其實過程很簡單:一直往左走 root->left->left->left...->null,由於是先序遍歷,因此一遇到節點,便需要立即訪問;由於一直走到最左邊後,需要逐步返回到父節點訪問右節點,因此必須有乙個措施能夠對節點序列回溯。有兩個辦法:

1.用棧記憶:在訪問途中將依次遇到的節點儲存下來。由於節點出現次序與恢復次序是反序的,因此是乙個先進後出結構,需要用棧。

使用棧記憶的實現有兩個版本。第乙個版本是模擬遞迴的實現效果,跟lx討論的,第二個版本是直接模擬遞迴。

2.節點增加指向父節點的指標:通過指向父節點的指標來回溯(後來發現還要需要增加乙個訪問標誌,來指示節點是否已經被訪問,不知道可不可以不用標誌直接實現回溯?想了一下,如果不用這個標誌位,回溯的過程會繁瑣很多。暫時沒有更好的辦法。)

(還有其他辦法可以回溯麼?)

這3個演算法偽**如下,沒有測試過。

先序遍歷偽**:非遞迴版本,用棧實現,版本1

// 先序遍歷偽**:非遞迴版本,用棧實現,版本1

void  preorder1(tnode* root)

else }

} preorder1每次都將遇到的節點壓入棧,當左子樹遍歷完畢後才從棧中彈出最後乙個訪問的節點,訪問其右子樹。在同一層中,不可能同時有兩個節 點壓入棧,因此棧的大小空間為o(h),h為二叉樹高度。時間方面,每個節點都被壓入棧一次,彈出棧一次,訪問一次,複雜度為o(n)

先序遍歷偽**:非遞迴版本,用棧實現,版本2

// 先序遍歷偽**:非遞迴版本,用棧實現,版本2

void  preorder2(tnode* root) }

} preorder2每次將節點壓入棧,然後彈出,壓右子樹,再壓入左子樹,在遍歷過程中,遍歷序列的右節點依次被存入棧,左節點逐次被訪問。同一時 刻,棧中元素為m-1個右節點和1個最左節點,最高為h。所以空間也為o(h);每個節點同樣被壓棧一次,彈棧一次,訪問一次,時間複雜度o(n)

先序遍歷偽**:非遞迴版本,不用棧,增加指向父節點的指標

// 先序遍歷偽**:非遞迴版本,不用棧,增加指向父節點的指標

void  preorder3(tnode* root)

if  ( root->left != null && !root->left->bvisited )       // 訪問左子樹

else

if ( root->right != null && !root->right->bvisited )  // 訪問右子樹

else

// 回溯 }

} preorder3的關鍵在於回溯。為了回溯增加指向父親節點的指標,以及是否已經訪問的標誌位,對比preorder1與preorder2,但 增加的空間複雜度為o(n)。時間方面,每個節點被訪問一次。但是,當由葉子節點跳到下乙個要訪問的節點時,需要先回溯至父親節點,再判斷是否存在沒有被 訪問過的右子樹,如果沒有,則繼續回溯,直至找到一顆沒有被訪問過的右子樹,這個過程需要很多的時間。每個葉子節點的回溯需要o(h)時間複雜度,葉子節 點最多為(2^(h-1)),因此回溯花費的上限為o(h*(2^(h-1))。這個上限應該可以縮小。preorder3唯一的好處是不需要額外的資料 結構-棧。

2.中序遍歷

根據上面的先序遍歷,可以類似的構造出中序遍歷的三種方式。仔細想一下,只有第一種方法改過來時最方便的。需要的改動僅僅調換一下節點訪問的次序,先序是先訪問,再入棧;而中序則是先入棧,彈棧後再訪問。偽**如下。時間複雜度與空間複雜度同先序一致。

// 中序遍歷偽**:非遞迴版本,用棧實現,版本1

void  inorder1(tnode* root)

if  ( !s.empty() ) }

} 第二個用棧的版本卻並不樂觀。preorder2能夠很好的執行的原因是,將左右節點壓入棧後,根節點就再也用不著了;而中序和後序卻不一樣,左右 節點入棧後,根節點後面還需要訪問。因此三個節點都要入棧,而且入棧的先後順序必須為:右節點,根節點,左節點。但是,當入棧以後,根節點與其左右子樹的 節點就分不清楚了。因此必須引入乙個標誌位,表示 是否已經將該節點的左右子樹入棧了。每次入棧時,根節點標誌位為true,左右子樹標誌位為false。

偽**如下:

// 中序遍歷偽**:非遞迴版本,用棧實現,版本2

void  inorder2(tnode* root)

while  ( !s.empty() )

else

node->bpushed =  true ;   // 根節點標誌位為true

s.push(node);

if  ( node->left != null ) }

} }

對比先序遍歷,這個演算法需要額外的增加o(n)的標誌位空間。另外,棧空間也擴大,因為每次壓棧的時候都壓入根節點與左右節點,因此棧空間為 o(n)。時間複雜度方面,每個節點壓棧兩次,作為子節點壓棧一次,作為根節點壓棧一次,彈棧也是兩次。因此無論從哪個方面講,這個方法效率都不及inorder1。

至於不用棧來實現中序遍歷。頭暈了,暫時不想了。後面再來完善。還有後序遍歷,貌似更複雜。對了,還有個層序遍歷。再寫一篇吧。頭都大了。

9.8續

中序遍歷的第三個非遞迴版本:採用指向父節點的指標回溯。這個與先序遍歷是非常類似的,不同之處在於,先序遍歷只要一遇到節點,那麼沒有被訪問那麼 立即訪問,訪問完畢後嘗試向左走,如果左孩子補課訪問,則嘗試右邊走,如果左右皆不可訪問,則回溯;中序遍歷是先嘗試向左走,一直到左邊不通後訪問當前節 點,然後嘗試向右走,右邊不通,則回溯。(這裡不通的意思是:節點不為空,且沒有被訪問過)

// 中序遍歷偽**:非遞迴版本,不用棧,增加指向父節點的指標

void  inorder3(tnode* root)

if  ( !root->bvisited )

if  ( root->right != null && !root->right->bvisited )

else }

} 這個演算法時間複雜度與空間複雜度與第3個先序遍歷的版本是一樣的。

3.後序 遍歷

從直覺上來說,後序遍歷對比中序遍歷難度要增大很多。因為中序遍歷節點序列有一點的連續性,而後續遍歷則感覺有一定的跳躍性。先左,再 右,最後才中間節點;訪問左子樹後,需要跳轉到右子樹,右子樹訪問完畢了再回溯至根節點並訪問之。這種序列的不連續造成實現前面先序與中序類似的第1個與 第3個版本比較困難。但是按照第2個思想,直接來模擬遞迴還是非常容易的。如下:

// 後序遍歷偽**:非遞迴版本,用棧實現

void  postorder(tnode* root)

while  ( !s.empty() )

else

if  ( node->left != null )

node->bpushed =  true ;             // 根節點標誌位為true

} } }

和中序遍歷的第2個版本比較,僅僅只是把左孩子入棧和根節點入棧順序調換一下;這種差別就跟遞迴版本的中序與後序一樣。

4.層序遍歷

這個很簡單,就不說老。

// 層序遍歷偽**:非遞迴版本,用佇列完成

void  levelorder(tnode *root)

if  (null != node->right)  // 右孩子入隊 }

} 小結一下:

用棧來實現比增加指向父節點指標回溯更方便;

採用第乙個思想,就是跟蹤指標移動 用棧儲存中間結果的實現方式,先序與中序難度一致,後序很困難。先序與中序只需要修改一下訪問的位置即可。

採 用第二個思想,直接用棧來模擬遞迴,先序非常簡單;而中序與後序難度一致。先序簡單是因為節點可以直接訪問,訪問完畢後無需記錄。而中序與後序時,節點在 彈棧後還不能立即訪問,還需要等其他節點訪問完畢後才能訪問,因此節點需要設定標誌位來判定,因此需要額外的o(n)空間。

二叉樹遍歷(遞迴 非遞迴)

二叉樹以及對二叉樹的三種遍歷 先根,中根,後根 的遞迴遍歷演算法實現,以及先根遍歷的非遞迴實現。node public class node public node left public node right public object value 遍歷訪問操作介面 public inte ce ...

二叉樹非遞迴遍歷

二叉樹非遞迴遍歷的幾個要點 1 不管前序 中序還是後序,它們的遍歷路線 或者說是回溯路線,先沿左邊一直走到盡頭,然後回溯到某節點,並跳轉到該節點的右孩子 如果有的話 然後又沿著這個有孩子的左邊一直走到盡頭 都是一樣的。2 明確每次回溯的目的。比如,前序回溯的目的是為了訪問右子樹 中序回溯的目的是為了...

非遞迴遍歷二叉樹

中序遞迴遍歷 void inordertrvdigui node pnode 然而,當樹的深度很大 比如16 時 假設為滿二叉樹 樹的節點數為 2 0 2 1 2 2 2 15 2 16 65536,遍歷整個二叉樹意味著有65536次函式呼叫,這將極大地增加程式執行時間。這時,應該採取非遞迴便利二叉...