二叉樹遍歷 Morris O(1)空間複雜度
本文主要解決一個問題,如何實現二叉樹的前中後序遍歷,有兩個要求:
1. O(1)空間複雜度,即只能使用常數空間;
2. 二叉樹的形狀不能被破壞(中間過程允許改變其形狀)。
通常,實現二叉樹的前序(preorder)、中序(inorder)、後序(postorder)遍歷有兩個常用的方法:一是遞迴(recursive),二是使用棧實現的迭代版本(stack+iterative)。這兩種方法都是O(n)的空間複雜度(遞迴本身佔用stack空間或者使用者自定義的stack),所以不滿足要求。(用這兩種方法實現的中序遍歷實現可以參考這裡。)
Morris Traversal方法可以做到這兩點,與前兩種方法的不同在於該方法只需要O(1)空間,而且同樣可以在O(n)時間內完成。
要使用O(1)空間進行遍歷,最大的難點在於,遍歷到子節點的時候怎樣重新返回到父節點(假設節點中沒有指向父節點的p指標),由於不能用棧作為輔助空間。為了解決這個問題,Morris方法用到了線索二叉樹(threaded binary tree)的概念。在Morris方法中不需要為每個節點額外分配指標指向其前驅(predecessor)和後繼節點(successor),只需要利用葉子節點中的左右空指標指向某種順序遍歷下的前驅節點或後繼節點就可以了。
Morris只提供了中序遍歷的方法,在中序遍歷的基礎上稍加修改可以實現前序,而後續就要再費點心思了。所以先從中序開始介紹。
首先定義在這篇文章中使用的二叉樹節點結構,即由val,left和right組成:
1 struct TreeNode { 2 int val; 3 TreeNode *left; 4 TreeNode *right; 5 TreeNode(int x) : val(x), left(NULL), right(NULL) {} 6 };
一、中序遍歷
步驟:
1. 如果當前節點的左孩子為空,則輸出當前節點並將其右孩子作為當前節點。
2. 如果當前節點的左孩子不為空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。
a) 如果前驅節點的右孩子為空,將它的右孩子設定為當前節點。當前節點更新為當前節點的左孩子。
b) 如果前驅節點的右孩子為當前節點,將它的右孩子重新設為空(恢復樹的形狀)。輸出當前節點。當前節點更新為當前節點的右孩子。
3. 重複以上1、2直到當前節點為空。
圖示:
下圖為每一步迭代的結果(從左至右,從上到下),cur代表當前節點,深色節點表示該節點已輸出。
程式碼:
1 void inorderMorrisTraversal(TreeNode *root) { TreeNode *cur = root, *prev = NULL; 3 while (cur != NULL) 4 { 5 if (cur->left == NULL) // 1. 6 { 7 printf("%d ", cur->val); 8 cur = cur->right; 9 } 10 else 11 { 12 // find predecessor 13 prev = cur->left; 14 while (prev->right != NULL && prev->right != cur) 15 prev = prev->right; 16 17 if (prev->right == NULL) // 2.a) 18 { 19 prev->right = cur; 20 cur = cur->left; 21 } 22 else // 2.b) 23 { 24 prev->right = NULL; 25 printf("%d ", cur->val); 26 cur = cur->right; 27 } 28 } 29 } 30 }
複雜度分析:
空間複雜度:O(1),因為只用了兩個輔助指標。
時間複雜度:O(n)。證明時間複雜度為O(n),最大的疑惑在於尋找中序遍歷下二叉樹中所有節點的前驅節點的時間複雜度是多少,即以下兩行程式碼:
1 while (prev->right != NULL && prev->right != cur) 2 prev = prev->right;
直覺上,認為它的複雜度是O(nlgn),因為找單個節點的前驅節點與樹的高度有關。但事實上,尋找所有節點的前驅節點只需要O(n)時間。n個節點的二叉樹中一共有n-1條邊,整個過程中每條邊最多隻走2次,一次是為了定位到某個節點,另一次是為了尋找上面某個節點的前驅節點,如下圖所示,其中紅色是為了定位到某個節點,黑色線是為了找到前驅節點。所以複雜度為O(n)。
二、前序遍歷
前序遍歷與中序遍歷相似,程式碼上只有一行不同,不同就在於輸出的順序。
步驟:
1. 如果當前節點的左孩子為空,則輸出當前節點並將其右孩子作為當前節點。
2. 如果當前節點的左孩子不為空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。
a) 如果前驅節點的右孩子為空,將它的右孩子設定為當前節點。輸出當前節點(在這裡輸出,這是與中序遍歷唯一一點不同)。當前節點更新為當前節點的左孩子。
b) 如果前驅節點的右孩子為當前節點,將它的右孩子重新設為空。當前節點更新為當前節點的右孩子。
3. 重複以上1、2直到當前節點為空。
圖示:
1 void preorderMorrisTraversal(TreeNode *root) { 2 TreeNode *cur = root, *prev = NULL; 3 while (cur != NULL) 4 { 5 if (cur->left == NULL) 6 { 7 printf("%d ", cur->val); 8 cur = cur->right; 9 } 10 else 11 { 12 prev = cur->left; 13 while (prev->right != NULL && prev->right != cur) 14 prev = prev->right; 15 16 if (prev->right == NULL) 17 { 18 printf("%d ", cur->val); // the only difference with inorder-traversal 19 prev->right = cur; 20 cur = cur->left; 21 } 22 else 23 { 24 prev->right = NULL; 25 cur = cur->right; 26 } 27 } 28 } 29 }
複雜度分析:
時間複雜度與空間複雜度都與中序遍歷時的情況相同。
三、後序遍歷
後續遍歷稍顯複雜,需要建立一個臨時節點dump,令其左孩子是root。並且還需要一個子過程,就是倒序輸出某兩個節點之間路徑上的各個節點。
步驟:
當前節點設定為臨時節點dump。
1. 如果當前節點的左孩子為空,則將其右孩子作為當前節點。
2. 如果當前節點的左孩子不為空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點。
a) 如果前驅節點的右孩子為空,將它的右孩子設定為當前節點。當前節點更新為當前節點的左孩子。
b) 如果前驅節點的右孩子為當前節點,將它的右孩子重新設為空。倒序輸出從當前節點的左孩子到該前驅節點這條路徑上的所有節點。當前節點更新為當前節點的右孩子。
3. 重複以上1、2直到當前節點為空。
圖示:
程式碼:
1 void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'. 2 { 3 if (from == to) 4 return; 5 TreeNode *x = from, *y = from->right, *z; 6 while (true) 7 { 8 z = y->right; 9 y->right = x; 10 x = y; 11 y = z; 12 if (x == to) 13 break; 14 } 15 } 16 17 void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'. 18 { 19 reverse(from, to); 20 21 TreeNode *p = to; 22 while (true) 23 { 24 printf("%d ", p->val); 25 if (p == from) 26 break; 27 p = p->right; 28 } 29 30 reverse(to, from); 31 } 32 33 void postorderMorrisTraversal(TreeNode *root) { 34 TreeNode dump(0); 35 dump.left = root; 36 TreeNode *cur = &dump, *prev = NULL; 37 while (cur) 38 { 39 if (cur->left == NULL) 40 { 41 cur = cur->right; 42 } 43 else 44 { 45 prev = cur->left; 46 while (prev->right != NULL && prev->right != cur) 47 prev = prev->right; 48 49 if (prev->right == NULL) 50 { 51 prev->right = cur; 52 cur = cur->left; 53 } 54 else 55 { 56 printReverse(cur->left, prev); // call print 57 prev->right = NULL; 58 cur = cur->right; 59 } 60 } 61 } 62 }轉自http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html。
相關推薦
二叉樹遍歷 Morris O(1)空間複雜度
本文主要解決一個問題,如何實現二叉樹的前中後序遍歷,有兩個要求: 1. O(1)空間複雜度,即只能使用常數空間; 2. 二叉樹的形狀不能被破壞(中間過程允許改變其形狀)。 通常,實現二叉樹的前序(preorder)、中序(inorder)、後序(postorder
面試中很值得聊的二叉樹遍歷方法——Morris遍歷
Morris遍歷 通過利用空閒指標的方式,來節省空間。時間複雜度O(N),額外空間複雜度O(1)。普通的非遞迴和遞迴方法的額外空間和樹的高度有關,遞迴的過程涉及到系統壓棧,非遞迴需要自己申請棧空間,都具有O(N)的額外空間複雜度。 Morris遍歷的原則: 1. 假設當前節點為cur, 2. 如果cur沒有左
二叉樹遍歷 C#
這就是 中序 工作 class stat public 完全 每一個 前期準備 二叉樹遍歷 C# 什麽是二叉樹 二叉樹是每個節點最多有兩個子樹的樹結構 (1)完全二叉樹——若設二叉樹的高度為h,除第 h 層外,其它各層 (1~h-1) 的結
[javaSE] 數據結構二叉樹-遍歷與查找
ngx quest wan ase ngs san http zhong ros %E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A8%8B%E5%BA%8F%E7%9A%84%E6%80%9D%E7%BB%B4%E9%80%BB%E8%BE%91%2018%
二叉樹遍歷非遞歸算法——中序遍歷
spa tdi str max logs nor 算法實現 中序遍歷 非遞歸 二叉樹中序遍歷的非遞歸算法同樣可以使用棧來實現,從根結點開始,將根結點的最左結點全部壓棧,當結點p不再有最左結點時,說明結點p沒有左孩子,將該結點 出棧,訪問結點p,然後對其右孩子做同樣的處理
HDU 1710Binary Tree Traversals(已知前序中序,求後序的二叉樹遍歷)
pid http pan clu names pty efi images 樹遍歷 題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1710 解題思路:可以由先序和中序的性質得到 : 先序的第一個借點肯定是當前子樹的根結點, 那
STL實現二叉樹遍歷
nod 數據 blog new friend const turn ace lrn #include<iostream> using namespace std; template<class Type> class BSTree; templat
二叉樹遍歷
while nbsp .net right 三種 pos tail stack實現 order 二叉樹遍歷最簡單的就是遞歸了。因為遞歸實質上是棧存了一些中間值,所以我們可以使用stack實現叠代版的遍歷。 中序遍歷 步驟: 首先將root節點作為當前節點。
二叉樹——遍歷篇(c++)
比較 方便 || 遍歷二叉樹 找到 保存 們的 order out 二叉樹——遍歷篇 二叉樹很多算法題都與其遍歷相關,筆者經過大量學習並進行了思考和總結,寫下這篇二叉樹的遍歷篇。 1、二叉樹數據結構及訪問函數 #include <stdio.h> #includ
二叉樹遍歷算法總結
使用 preorder 說明 stack height type pri content 結構圖 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
二叉樹 - 遍歷和存儲結構
前序遍歷 main inorder esp bottom ive align return c 編程 在《二叉樹的定義和性質》中我們已經認識了二叉樹這種數據結構。我們知道鏈表的每個節點可以有一個後繼,而二叉樹(Binary Tree)的每個節點可以有兩個後繼。比如這樣定義二
【樹】二叉樹遍歷算法(深度優先、廣度優先遍歷,前序、中序、後序、層次)及Java實現
order new link left 算法 很多 == 都是 off 二叉樹是一種非常重要的數據結構,很多其它數據結構都是基於二叉樹的基礎演變而來的。對於二叉樹,有深度遍歷和廣度遍歷,深度遍歷有前序、中序以及後序三種遍歷方法,廣度遍歷即我們平常所說的層次遍歷。因為樹的定義
二叉樹遍歷(先序、中序、後序)
廣度 nsh 直接 循環 ron color lan 通過 ogr 二叉樹的遍歷(遞歸與非遞歸) 遍歷:traversal 遞歸:recursion 棧----------回溯----------遞歸 棧和回溯有關 本文討論二叉樹的常見遍歷方式的代碼(Java)實現,包括
二叉樹遍歷(王道)
中序 數組 har 不為 位置 mem 一行 遍歷 uil 題目描述: 二叉樹的前序、中序、後序遍歷的定義:前序遍歷:對任一子樹,先訪問跟,然後遍歷其左子樹,最後遍歷其右子樹;中序遍歷:對任一子樹,先遍歷其左子樹,然後訪問根,最後遍歷其右子樹;後序遍歷:對任一子樹,先遍歷其
【二叉樹】二叉樹遍歷總結
struct left else oot nor 節點 操作 preorder AC 節點定義如下 1 // Definition for a binary tree node. 2 struct TreeNode { 3 int val; 4 Tre
一道關於二叉樹遍歷的題目
inf 一道 .com 順序 序列 技術 題目 bubuko 圖片 以下序列中不可能是一顆二叉查找樹的後序遍歷結構的是:(B) A: 1,2,3,4,5 B: 3,5,1,4,2 C: 1,2,5,4,3 D: 5,4,3,2,1 二叉查找樹:左子樹比根小,右子樹比根大後序
c++實現二叉樹層序、前序創建二叉樹,遞歸非遞歸實現二叉樹遍歷
log ios cst ack ret 出棧 隊列 結點 非遞歸實現 #include <iostream> #include <cstdio> #include <stdio.h> #include <string> #i
二叉樹遍歷規則,先順遍歷/中序遍歷/後序遍歷
子節點 itl 根據 得到 mar spa 先序遍歷 bubuko 中序 二叉樹三種遍歷方式 先序遍歷:遍歷順序規則為【根左右】 先訪問根節點,在左葉子,右葉子 中序遍歷:遍歷順序規則為【左根右】 後序遍歷:遍歷順序規則為【左右根】 例題 先序遍歷:ABCDEFGHK
二叉樹遍歷之遞迴演算法
作者:石鍋拌飯 原文連結 二叉樹的遍歷演算法有多種,典型的有先序遍歷、中序遍歷、後序遍歷以及層序遍歷。而且這些遍歷的遞迴演算法較為簡單,程式碼很少,容易實現,本文就是彙總二叉樹遍歷的遞迴演算法,非遞迴演算法將在下一篇文章中進行總結。本文中用到的二叉樹例項如下:
python 二叉樹遍歷 DFS和BFS
檢查python 版本 import sys print(sys.version) print(sys.version_info ) mac python 自己寫的資料結構在 Documents/data_structure/python中 Document