# Invert binary tree in O(1) space without stack/queue/recursion contributed by < `fennecJ` > 昨天聽了 Jserv 開的直播--[資訊科技產業專案設計](https://youtu.be/Bk5O9xPPf0c?t=9265) ,裡面提到了一個很有趣的問題 : <br> 有沒有辦法不用 recursion 並 Invert binary tree in O(1) space ?( 即無法使用 stack/queue 等額外的資料結構輔助 ) 要回答這個問題,我們可以先從 Morris traversal 開始看起。 ## Morris traversal 核心精神 : 以 leaf node 作為回到 root 的入口 以往我們在進行 binary tree 的 traversal 時,我們之所以需要用到 recursion 或是 stack/queue 的輔助是因為我們需要有一個方法可以在進入 child node 之後,回到原本的 parent node 。 而 Morris traversal 對這個問題提供了一個解法 : 在 leaf node 的地方新增一個回到 parent 的 link ( 有的人可能會直覺聯想到 [Threaded binary tree](https://www.geeksforgeeks.org/threaded-binary-tree/) ,但兩者仍有決定性的不同, Threaded binary tree 需要在結構內額外開位子存當前 node 的 child 是否為連到 ancestor 的 thread ,而 morris 的做法則不用。 ) 我們來看看他的 pseudo code (Morris traversal Inorder) , 我參考這篇[文章](https://www.geeksforgeeks.org/inorder-tree-traversal-without-recursion-and-without-stack/)做了少許改編 : <br> * Initialize cur as root * While cur != NULL * If the cur->left == NULL * a) Print cur’s data * b) cur = cur->right * Else * Find rightmost node in cur's left subtree which right child != cur. * If rightmost node's right child != NULL (it must be cur) ***//Which means there is a link to the cur.*** * Let rightmost node's right child = NULL ***//Break the link*** * Print cur's data * cur = cur->right. * Else * Let rightmost node's right child = cur ***//Build a link*** * cur = cur->left. 觀察上述 pseudo code,第一次進入 tree 的時候,由於沒有 link 存在, cur 會一直往 cur->left 走,直到到 leftmost node(cur->left == NULL) ,才藉由 link 往 parent 走,之後往 parent->right 移動。順序是 left->parent->right ,為 inOrder traversal 的順序。 以下放上熱心網友做的圖[^first]幫助理解: ![](https://i.imgur.com/yC6fP9j.png) 也可以看熱心網友做的動畫[^second]: ![](https://i.imgur.com/ibgmMiC.gif) 實作程式碼 : ```c= void morris_inOrder(node *head) { node *cur = head; while (cur) { node *q = cur->left; if (q) { while (q->right && q->right != cur) q = q->right; if (!q->right) { q->right = cur; cur = cur->left; continue; } q->right = NULL; printf("%d ", cur->data); cur = cur->right; continue; } printf("%d ", cur->data); cur = cur->right; } } ``` 空間複雜度 : O(1)<br> 時間複雜度 : O(2n) ,因為每個 node 最多只會經過兩次<br> 事實上,只要在將第 15 行的 ```c printf("%d ", cur->data); ``` 移到第 10 行後面 <br> 就可以將其改為 preOrder: ```c= void morris_preOrder(node *head) { node *cur = head; while (cur) { node *q = cur->left; if (q) { while (q->right && q->right != cur) q = q->right; if (!q->right) { q->right = cur; printf("%d ", cur->data); cur = cur->left; continue; } q->right = NULL; cur = cur->right; continue; } printf("%d ", cur->data); cur = cur->right; } } ``` 至於 postOrder ,若要用 morris alg 實作,會稍微複雜一些,這裡暫不討論,有興趣的人可以參考[這篇](https://www.quora.com/What-is-a-good-way-to-implement-stackless-recursion-less-post-order-traversal-for-a-non-threaded-binary-tree-using-Morris-method) 接下來我們回來看看題目。 # Invert binary tree in O(1) space 以 inOrder 或是 preOrder 順序對 tree 進行 invert,若在當前 node 的所有 children 尚未遍歷之前就發生 invert 則會導致原本 left -> root -> right 順序變成 left -> root -> left( 因為 root invert 後原本的 left 和 right child 會互換 ) 這樣 cur 究竟要往左或往右走不太好處理。 <br> 我們要確保對一 node invert 之前其所有 children 都遍歷過了。<br> 在 morris traversal 的基礎上進行改編,得到以下程式碼 ( 由 [此篇文章](https://maskray.me/blog/2015-06-16-morris-post-order-traversal) 改編 ) ```c= void invert_bin_tree(node *root) { node *cur = new_node(0); cur->left = root; while (cur) { node *q = cur->left; if (q) { while (q->right && q->right != cur) q = q->right; if (!q->right) { q->right = cur; cur = cur->left; continue; } node *tmp = cur->left; while (tmp) { swap(&tmp->left, &tmp->right); if (tmp == q) break; tmp = tmp->left; } q->left = NULL; /* Link from q to cur is changed from q->right into q->left, causing by while loop for tmp in line 15. */ } cur = cur->right; } } ``` 不同的地方有幾個點: * 多了一個 dummy node 指向 root * 第 12 行原本 break link 應為 `q->right = NULL;` 被改為 `q->left = NULL;` 之所以多了一個 dummy node 並指向 root ,是為了讓 cur 走到 right most node 時,能藉這個 node 走回 root,並對 root 進行 invert 。<br> 而為何從 `q->right = NULL;` 改為 `q->left = NULL;`<br> 是因為在前面的 while(tmp) 中, 會對 tmp 的 left child 以及 right child 進行交換,直到 tmp == q 為止進行最後一次交換。<br> 此時 q 的 left 和 right child 進行交換,導致原本指向 cur 的 right child 變成 left child ,因此要打破這個 link 要用 `q->left = NULL;` 我嘗試做了一個動畫幫助理解 : <br> 可以在[這裡](https://docs.google.com/presentation/d/1sjgbtara7MMqqIXxtd2xf4L0QLjOqJTeBzuH2FPn6hk/edit?usp=sharing)一頁一頁翻 ![](https://i.imgur.com/qLOvlBB.gif) 那麼,有沒有辦法可以不額外新增一個 dummy node 呢 ? <br> 近日我在 leetcode 討論區看到 [這個解答](https://leetcode.com/problems/invert-binary-tree/discuss/62781/C%2B%2B-Non-recursive-O(n)-time-O(1)-space-solution-based-on-Morris-Traversal) 正如前面所提,如果用原本的 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 ```c= void swap_LR_morris_inOrder(node *head) { node *cur = head; while (cur) { node *q = cur->left; if (q) { while (q->left && q->left != cur) q = q->left; if (!q->left) { q->left = cur; cur = cur->right; continue; } q->left = NULL; swap(&cur->left, &cur->right); cur = cur->left; continue; } swap(&cur->left, &cur->right); cur = cur->left; } } ``` 接著在改編後的基礎上加回未翻轉的 morris_alg ,用以下 code 取代原本第 9 行的 `if (!q->left) ` block 的內容 ```c= q = cur->left; // 回到原本 morris_alg 的起點 while (q->right && q->right != cur) q = q->right; if (!q->right) { q->right = cur; cur = cur->left; continue; } ``` 最後我們就得到了以下 code : ```c= void invert_bin_tree(node *head) { node *cur = head; while (cur) { node *q = cur->left; if (q) { while (q->left && q->left != cur) q = q->left; if (!q->left) { q = cur->left; // 回到原本 morris_alg 的起點 while (q->right && q->right != cur) q = q->right; if (!q->right) { q->right = cur; cur = cur->left; continue; } } q->left = NULL; swap(&cur->left, &cur->right); cur = cur->left; continue; } swap(&cur->left, &cur->right); cur = cur->left; } } ``` 再來我們對上面的 code 進行簡化,初步觀察可以發現第 `20~22` 行是多餘的,把他刪掉,變成 : ```c= void invert_bin_tree(node *head) { node *cur = head; while (cur) { node *q = cur->left; if (q) { while (q->left && q->left != cur) q = q->left; if (!q->left) { q = cur->left; // 回到原本 morris_alg 的起點 while (q->right && q->right != cur) q = q->right; if (!q->right) { q->right = cur; cur = cur->left; continue; } } q->left = NULL; } swap(&cur->left, &cur->right); cur = cur->left; } } ``` 接下來我們開始檢視上面的 code 究竟做了什麼 ? <br> 首先一樣從 cur 的 left subtree 開始進行 (`node *q = cur->left;`) <br> 再來是如何判斷進入 cur 之前已經發生了翻轉,由於我們建立 link 時只會從 leaf node 的 ==right== child 連,因此若在 cur 的 children 中存在 node 的 ==left== child 能連回 cur 的話表示這個 node( 作為 cur 的 parent) 已經被翻轉過了。第 9 行的 `if (!q->left)` 就是用來判斷是否有發生翻轉的條件。 <br> 另外,若存在符合以上描述的 node ,此 node 必為 subtree 中的 leftmost node ,因為 cur 第一次進入一個 subtree 時必會一直走到 leftmost node ,之後再藉 link 往回走 ( 第一次進入時狀態為未翻轉,會一直執行第 15 行,往 cur->left 移動 ) 。 <br> 可觀察以下動畫幫助理解 : ![](https://i.imgur.com/H0acw3K.gif) 也可在 [這裡](https://docs.google.com/presentation/d/1FiUpldrtOE6NKB75fe-ebRfIi7vUwEZ3zi_JpC3vIE4/edit?usp=sharing) 一頁一頁翻 <br> 基本上以上就是以 morris_alg 進行 invert bin tree 而不用到 dummy node 的做法。 <br> 最後, cur 走回 parent 唯一的路徑只有在 swap 後第 22 行的 `cur = cur->left;` ,因此只要進入 `!q->left` 的區塊,表示目前是 cur 第一次進入該 subtree ,不會事先有 link 存在,第 11 行的 `&& q->right != cur` 以及第 13 行的 `if (!q->right)` 是可以直接拔掉的。 ```c= void invert_bin_tree3(node *root) { node *cur = root; while (cur) { node *q = cur->left; if (q) { while (q->left && q->left != cur) q = q->left; if (!q->left) { q = cur->left; while (q->right) q = q->right; q->right = cur; cur = cur->left; continue; } q->left = NULL; } swap(&cur->right, &cur->left); cur = cur->left; } } ``` 以上 code 皆可到 [我的 Github](https://github.com/fennecJ/alg_practice/blob/master/morris_traversal.c) 上下載,最後附上 [LeetCode 226.](https://leetcode.com/problems/invert-binary-tree/) 的解答 : ```c= /** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */ inline void swap(struct TreeNode **a, struct TreeNode **b) { struct TreeNode *tmp = *a; *a = *b; *b = tmp; } struct TreeNode* invertTree(struct TreeNode* root){ struct TreeNode *cur = root; while (cur) { struct TreeNode *q = cur->left; if (q) { while (q->left && q->left != cur) q = q->left; if (!q->left) { q = cur->left; while (q->right) q = q->right; q->right = cur; cur = cur->left; continue; } q->left = NULL; } swap(&cur->left, &cur->right); cur = cur->left; } return root; } ``` #### 參考資料 * [Inorder Tree Traversal without recursion and without stack!](https://www.geeksforgeeks.org/inorder-tree-traversal-without-recursion-and-without-stack/) * [Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)](https://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html) * [Explain Morris inorder tree traversal without using stacks or recursion](https://stackoverflow.com/a/58191797/16028494) * [What is a good way to implement stackless/recursion less post order traversal for a non threaded binary tree using Morris method?](https://www.quora.com/What-is-a-good-way-to-implement-stackless-recursion-less-post-order-traversal-for-a-non-threaded-binary-tree-using-Morris-method) * [常數空間Invert Binary Tree與仿Morris法後序遍歷](https://maskray.me/blog/2015-06-16-morris-post-order-traversal) * [C++ Non-recursive O(n) time O(1) space solution, based on Morris Traversal](https://leetcode.com/problems/invert-binary-tree/discuss/62781/C%2B%2B-Non-recursive-O(n)-time-O(1)-space-solution-based-on-Morris-Traversal) [^first]: [Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)](https://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html) [^second]: [Explain Morris inorder tree traversal without using stacks or recursion](https://stackoverflow.com/a/58191797/16028494)