contributed by < fennecJ
>
昨天聽了 Jserv 開的直播–資訊科技產業專案設計 ,裡面提到了一個很有趣的問題 :
有沒有辦法不用 recursion 並 Invert binary tree in O(1) space ?( 即無法使用 stack/queue 等額外的資料結構輔助 )
要回答這個問題,我們可以先從 Morris traversal 開始看起。
核心精神 : 以 leaf node 作為回到 root 的入口
以往我們在進行 binary tree 的 traversal 時,我們之所以需要用到 recursion 或是 stack/queue 的輔助是因為我們需要有一個方法可以在進入 child node 之後,回到原本的 parent node 。
而 Morris traversal 對這個問題提供了一個解法 : 在 leaf node 的地方新增一個回到 parent 的 link ( 有的人可能會直覺聯想到 Threaded binary tree ,但兩者仍有決定性的不同, Threaded binary tree 需要在結構內額外開位子存當前 node 的 child 是否為連到 ancestor 的 thread ,而 morris 的做法則不用。 )
我們來看看他的 pseudo code (Morris traversal Inorder) ,
我參考這篇文章做了少許改編 :
觀察上述 pseudo code,第一次進入 tree 的時候,由於沒有 link 存在, cur 會一直往 cur->left 走,直到到 leftmost node(cur->left == NULL) ,才藉由 link 往 parent 走,之後往 parent->right 移動。順序是 left->parent->right ,為 inOrder traversal 的順序。
以下放上熱心網友做的圖[1]幫助理解: 也可以看熱心網友做的動畫[2]:
實作程式碼 :
空間複雜度 : O(1)
時間複雜度 : O(2n) ,因為每個 node 最多只會經過兩次
事實上,只要在將第 15 行的
移到第 10 行後面
就可以將其改為 preOrder:
至於 postOrder ,若要用 morris alg 實作,會稍微複雜一些,這裡暫不討論,有興趣的人可以參考這篇
接下來我們回來看看題目。
以 inOrder 或是 preOrder 順序對 tree 進行 invert,若在當前 node 的所有 children 尚未遍歷之前就發生 invert 則會導致原本 left -> root -> right 順序變成 left -> root -> left( 因為 root invert 後原本的 left 和 right child 會互換 )
這樣 cur 究竟要往左或往右走不太好處理。
我們要確保對一 node invert 之前其所有 children 都遍歷過了。
在 morris traversal 的基礎上進行改編,得到以下程式碼 ( 由 此篇文章 改編 )
不同的地方有幾個點:
q->right = NULL;
被改為 q->left = NULL;
之所以多了一個 dummy node 並指向 root ,是為了讓 cur 走到 right most node 時,能藉這個 node 走回 root,並對 root 進行 invert 。
而為何從 q->right = NULL;
改為 q->left = NULL;
是因為在前面的 while(tmp) 中, 會對 tmp 的 left child 以及 right child 進行交換,直到 tmp == q 為止進行最後一次交換。
此時 q 的 left 和 right child 進行交換,導致原本指向 cur 的 right child 變成 left child ,因此要打破這個 link 要用 q->left = NULL;
我嘗試做了一個動畫幫助理解 :
可以在這裡一頁一頁翻
那麼,有沒有辦法可以不額外新增一個 dummy node 呢 ?
近日我在 leetcode 討論區看到 這個解答
正如前面所提,如果用原本的 morris_alg 來做的話,難點在於判斷 cur 何時該往左走,何時該往右走,因此關鍵在於,如何判斷進入一個 node 前該 subtree 已經被翻轉過了 ( 即該 node 的 parent 已經被翻轉過了 )。如果被翻轉過了,那就用翻轉後的方法進行 morris alg ( left 和 right 互換 ) ,否則就照沒翻轉的情形進行 morris alg 。 我們將原本的 morris_inOrder left right 互換,但是仍維持從 cur 的 left subtree 開始處理,因此 q 的 init value 維持 cur->left
接著在改編後的基礎上加回未翻轉的 morris_alg ,用以下 code 取代原本第 9 行的 if (!q->left)
block 的內容
最後我們就得到了以下 code :
再來我們對上面的 code 進行簡化,初步觀察可以發現第 20~22
行是多餘的,把他刪掉,變成 :
接下來我們開始檢視上面的 code 究竟做了什麼 ?
首先一樣從 cur 的 left subtree 開始進行 (node *q = cur->left;
)
再來是如何判斷進入 cur 之前已經發生了翻轉,由於我們建立 link 時只會從 leaf node 的 right child 連,因此若在 cur 的 children 中存在 node 的 left child 能連回 cur 的話表示這個 node( 作為 cur 的 parent) 已經被翻轉過了。第 9 行的 if (!q->left)
就是用來判斷是否有發生翻轉的條件。
另外,若存在符合以上描述的 node ,此 node 必為 subtree 中的 leftmost node ,因為 cur 第一次進入一個 subtree 時必會一直走到 leftmost node ,之後再藉 link 往回走 ( 第一次進入時狀態為未翻轉,會一直執行第 15 行,往 cur->left 移動 ) 。
可觀察以下動畫幫助理解 :
也可在 這裡 一頁一頁翻
基本上以上就是以 morris_alg 進行 invert bin tree 而不用到 dummy node 的做法。
最後, cur 走回 parent 唯一的路徑只有在 swap 後第 22 行的 cur = cur->left;
,因此只要進入 !q->left
的區塊,表示目前是 cur 第一次進入該 subtree ,不會事先有 link 存在,第 11 行的 && q->right != cur
以及第 13 行的 if (!q->right)
是可以直接拔掉的。
以上 code 皆可到 我的 Github 上下載,最後附上 LeetCode 226. 的解答 :