iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 18
1
Software Development

從LeetCode學演算法系列 第 18

[Day 18] 從LeetCode學演算法 - 0094. Binary Tree Inorder Traversal (Medium)

目標:
這題主要目的在於介紹二元樹的中序走訪,
同時用stack結構來講解迭代解的思路。

原題:

Question:
Given a binary tree, return the inorder traversal of its nodes’ values.
Example:
Input: [1,null,2,3]
   1
    \
     2
    /
   3
Output: [1,3,2]
Follow up: Recursive solution is trivial, could you do it iteratively?

分析/解題:
給定一個二元樹,題目要求以inorder(中序)方式回傳其節點值。
由於上一題的關係(讀者可以回頭參照第16篇),
我們已經講解過In-order的方式了,
原則上就是經過左邊的部分->中間的部分->右邊的部分的順序。
請留意這題輸出並不一定會由小到大(因為並沒有限制是二元搜尋樹)。

我們先來嘗試遞迴解法,通常較為簡單,這邊重新列出上一篇提到的:

inorder(root) {
  if root == NIL return
  inorder(root.left)
  print(root.val) // Or add/append to the list
  inorder(root.right)
}

總體而言,每次我們都會走到當前所能走到的左邊
直到左邊再也沒有node(變成NIL)時,會回到上一層(return)
印出值(或將其放到list中),接著再往自身的右邊找
每次同樣的模式操作完以後,我們可以保證對於每個子樹,
都是先拿到其左邊的節點值(們),再來是自身,
最後才是右邊的節點值(們),從而保證這個演算法是得以遵照目標的順序完成的。

由於我們會使用一個ArrayList或List來存放inorder得到的值,
所以下面是採用先宣告再將其放置到另一個遞迴用函式中處理的方式。
當然,如讀者只想使用一個函式來遞迴,
也可以宣告一個Solution的class變數,這樣就不需要每次傳遞了。
兩者各有好處,讀者可依自己喜好決定。

Java:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inorder(root, res);
        return res;
    }
    public void inorder(TreeNode root, List<Integer> res) {
        if (root == null) return;
        inorder(root.left, res);
        res.add(root.val);
        inorder(root.right, res);
    }
}

Python:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        self.inorder(root, res)
        return res
        
    def inorder(self, root: TreeNode, res: List[int]) -> None:
        if not root: return
        self.inorder(root.left, res)
        res.append(root.val)
        self.inorder(root.right, res)

那麼,如果是要求要迭代解(iterative solution)呢?
我們回顧一下前面的作法,遞迴的時候之所以我們能回到上一層,
其實是因為我們的程式有記錄上一層的函式的執行狀態,
從而在return時能返回到應該繼續的那一行執行。
由於最後一層函式在返回時必須先被呼叫到
所以電腦在儲存時是採用Stack的形式(因為要後進先出)。
(這個堆疊又叫做The Stack或Call stack(呼叫堆疊))

因此,我們想要能夠不用遞迴解的話,
顯然我們應該要自己使用一個Stack來處理整個流程。

  1. 在In-order的流程中,首先我們是先一路走到最左邊的node,
    中途經過的node我們應該要將其放進Stack中,這樣才能回到上一層去,
    我們將這個node暫時命名為curr(current)。
  2. curr一路向左最終一定會遇到NIL,表示這排從root一路向左邊走,
    沿途的node全數放入到Stack中了
  3. 那麼我們應該要將最後一個node從Stack中拿出來
    (因為後進先出,最後一個node應該是最左邊的node才對),將其放到list當中。
  4. 接著將curr設為自己右邊的node。
  5. 反覆上面的流程,一直到curr和stack都空掉為止。

寫成虛擬碼如下:

create a list named res
create a stack named stack
curr = root
while ( curr != NIL or !stack.isEmpty() ) {
    while (curr != NIL) {
        stack.push(curr)
        curr = curr.left
    }
    curr = stack.pop()
    res.add(curr.val)
    curr = curr.right
}
return res

Java:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        TreeNode curr = root;
        while (curr != null || !stack.isEmpty()) {
            while (curr != null) {
                stack.push(curr);
                curr = curr.left;
            }
            curr = stack.pop();
            res.add(curr.val);
            curr = curr.right;
        }
        return res;
    }
}

Python:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        stack = []
        curr = root
        while curr or stack:
            while curr:
                stack.append(curr)
                curr = curr.left
            curr = stack.pop()
            res.append(curr.val)
            curr = curr.right
        return res

面試實際可能會遇到的問題:
「時間/空間複雜度?」
(O(N), O(N),因為要走訪過整棵樹,且list要儲存整個二元樹攤平的結果)

「如果我將res放在class Solution的level有什麼優缺點?」
(優點:
有機會速度快一點點,且通常不用傳位址給函式,因為直接能存取到。
缺點:
對整個class均為可見,如果這個函式是要被反複使用的話,
要注意初始化的問題,且也有可能被其他函式修改到。)
從LeetCode學演算法,我們下次見囉,掰~

下篇預告:
0124. Binary Tree Maximum Path Sum (Hard)


上一篇
[Day 17] 從LeetCode學演算法 - 0098. Validate Binary Search Tree (Medium)
下一篇
[Day 19] 從LeetCode學演算法 - 0124. Binary Tree Maximum Path Sum (Hard)
系列文
從LeetCode學演算法30

尚未有邦友留言

立即登入留言