1. 程式人生 > >94 Binary Tree Inorder Traversal 【遞迴和迭代的對比較分析】

94 Binary Tree Inorder Traversal 【遞迴和迭代的對比較分析】

一道很常規的二叉樹遍歷題,相信大家都在課上學習過。

但是題目要求是不能用遞迴呼叫的方法,也就是課上講過的方法。要用iterative迭代的方法,也就是一個一個找,通過while迴圈來輸出。

先把遞迴的方法程式碼寫出來,如下。要注意的是,我連遞迴呼叫都不能立馬想起來,只想到了當node是null時,什麼都不用做。那麼當不是null的時候呢?其實就是按照preorder的順序,擺放的三行程式碼即可,怎麼記憶呢?以單一root為例,就是先輸出左子節點,其為空,則不處理,然後到本身,然後到右子節點。

課程中一般只需要列印,但是這題要求把所有節點數值收集到一個集合裡,所以設計一個helper函式,不返回任何值 (void 函式在這裡比較好用來遞迴呼叫)。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result= new ArrayList<>();
        helper(root, result);
        return result;
    }
    // 課程中一般只需要打印出來,但是這裡需要返回一個list,那麼就寫一個helper函式,加入一個輸入的引數即能解決問題。
    public void helper(TreeNode root, List<Integer> list){
/*        if(root!=null){
            helper(root.left, list);
            list.add(root.val);
            helper(root.right, list);
        }*/
        if(root=null) return; // 這句code其實並不是很intuitive,意思是一樣的,如果為空,則不做任何處理,剛進入就返回。
        helper(root.left, list);
        list.add(root.val);
        helper(root.right, list);
    }
}

那麼用迭代的方法,思路是比較有意思的,同時也比較固定,沒有其他解法。

參考程式碼來理解整個邏輯:

1,從root開始要往深度走,找到最左邊的節點,那麼中間走過的這些節點都需要一次記錄下來,而且順序的話是要反向輸出的。用stack合適!

2,一直往最左走到了null,那麼stack中的top位置就是要彈出並且記入結果的node。然而這個node的右端是緊接著要讀取的部分,所以curr要指向這個node的右子節點,然後開始重複上面的往左走到最左,並且一直往stack裝node的過程。

大概的過程就是這樣,其實本質上是什麼?就是遞迴呼叫方法中,一模一樣的過程,因為在遞迴呼叫中,這些訪問過的節點也都是存放在系統中的stack裡,一個個壓入,然後當遇到return時再一個個彈出,只不過遞迴呼叫是把所有過程都隱藏起來了,而迭代的話,是顯示的一一寫出了。

程式碼中一個需要好好理解的就是第一個while的條件:當curr不為null,或者stack裡還有node時,就一直迴圈操作。只有當curr指向null,同時stack裡也沒有東西,那麼才代表整個樹遍歷完畢。在每一個迴圈中,curr還是變化的中心:1 往左走到底;2 指向最後左節點;3,指向最左節點的右子節點;然後再次進入迴圈。

以上的過程,最好是通過畫圖在腦海中形成印象:一棵樹,從根到最左,到右子,依然到最左,如果沒有,則指向stack中彈出來top element,等於是往上面爬了一層。具體畫面看ppt中的圖吧,文字也表示不清楚。

程式碼如下:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = 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 is null here after
            curr=stack.pop();
            result.add(curr.val);
            curr=curr.right;
        } // 想象一下最後curr的完結情況,最右端的節點,因此按照while迴圈,找到其最左子節點(null),然後pop彈出該節點
        // 寫入result,curr指向右節點,為null,此時stack也為空,整個樹遍歷完畢,迴圈也結束。
        
        return result;
    }
}

最後加一個 遞迴呼叫,但不需要helper方法的解法,之前說了如果是有return型別的方法遞迴呼叫,那麼可能不好操作,因為返回結果每次要new 一個list,但是其實把list裡所有的數全部add進去也是可以的,就用addAll就行:
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if(root==null) return result;
        result.addAll(inorderTraversal(root.left)); // 兩步寫作一行程式碼,有點意思,哈哈
        result.add(root.val);
        result.addAll(inorderTraversal(root.right)); // 同理
        return result;
    }
}

以上三組程式碼,效能效率是一樣的,充分說明了之前的判斷,recursive和iterative的本質是一樣的,遞迴只是系統自建stack,而迭代是把建stack,壓入,彈出這些過程全部顯示的寫成程式碼。

通過這題把二叉樹的遍歷算是搞的比較透徹。

之後兩篇分析 preorder 和 postorder