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次函式呼叫,這將極大地增加程式執行時間。這時,應該採取非遞迴便利二叉...