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