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]幫助理解:
實作程式碼 :
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)
時間複雜度 : O(2n) ,因為每個 node 最多只會經過兩次
事實上,只要在將第 15 行的
printf("%d ", cur->data);
移到第 10 行後面
就可以將其改為 preOrder:
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 實作,會稍微複雜一些,這裡暫不討論,有興趣的人可以參考這篇
接下來我們回來看看題目。
以 inOrder 或是 preOrder 順序對 tree 進行 invert,若在當前 node 的所有 children 尚未遍歷之前就發生 invert 則會導致原本 left -> root -> right 順序變成 left -> root -> left( 因為 root invert 後原本的 left 和 right child 會互換 )
這樣 cur 究竟要往左或往右走不太好處理。
我們要確保對一 node invert 之前其所有 children 都遍歷過了。
在 morris traversal 的基礎上進行改編,得到以下程式碼 ( 由 此篇文章 改編 )
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;
}
}
不同的地方有幾個點:
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
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 的內容
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 :
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
行是多餘的,把他刪掉,變成 :
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 究竟做了什麼 ?
首先一樣從 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)
是可以直接拔掉的。
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 上下載,最後附上 LeetCode 226. 的解答 :
/**
* 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;
}