No. 144 - Binary Tree Preorder Traversal ==== ![Problem](https://img.shields.io/badge/problem-tree-blue) ![Difficulty](https://img.shields.io/badge/difficulty-medium-yellow) --- ## [LeetCode 144 -- Binary Tree Preorder Traversal](https://leetcode.com/problems/binary-tree-inorder-traversal/) ### 題目描述 Given the root of a binary tree, return the preorder traversal of its nodes' values. Example 1. ![](https://i.imgur.com/Mp2OLGD.jpg) ``` Input: root = [1,null,2,3] Output: [1,2,3] ``` Example 2. ![](https://i.imgur.com/QtRtu53.jpg) ``` Input: root = [1,2] Output: [1,2] ``` --- 這裡我會分別講解遞迴 (recursive) 及疊代 (iterative) 的方法。 這題與 [94. Binary Tree Inorder Traversal](https://hackmd.io/QhRYCHmJQdu8NJb-lDVv-g) 概念類似,可以先看完 inorder traversal 的解法便能較快的理解 preorder traversal。 ### 解法 1. Recursive Approach Preorder traversal 就是***先走訪父節點,再走訪左子樹,再走訪右子樹***。 我們可以直接把上面的觀念寫成下面的程式碼: ```cpp= res.push_back(node->val); // 先走訪父節點 traversal(node->left, res); // 再走訪左子樹 traversal(node->right, res); // 再走訪右子樹 ``` 而 `traversal` 函式的作法如下所示,只有當走訪的 node 存在的時候才會繼續做 preorder traversal。 ```cpp= void traversal(TreeNode *node, vector<int> &res) { if (node) { res.push_back(node->val); traversal(node->left, res); traversal(node->right, res); } } ``` 有了 `traversal` 函式我們就可以直接呼叫這個函式完成我們的 preorder traversal。 ```cpp= /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: void traverse(TreeNode *node, vector<int> &res) { if (!node) return; res.push_back(node->val); traverse(node->left, res); traverse(node->right, res); } vector<int> preorderTraversal(TreeNode* root) { vector<int> res; traverse(root, res); return res; } }; ``` ### 解法 2. Iterative Approach 1 接著我們要用 iterative 的方法來做出 preorder traversal,其作法如下: ```cpp= /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> res; if (!root) return res; stack<TreeNode*> st; st.push(root); while (!st.empty()) { root = st.top(); res.push_back(root->val); st.pop(); if (root->right) st.push(root->right); if (root->left) st.push(root->left); } return res; } }; ``` 我們會需要一個 stack 來記錄每個走訪的 node,首先我們先將 root 丟進這個 stack。接著我們只要看這個 stack 裡面還有沒有 node,只要有就要把它 pop 出來存到 `res` 的 list,並且每一次 pop 就會把該 node 的左邊 node 跟右邊 node 丟到 stack 中,其順序是先丟右邊 node 再丟左邊 node,因為我們是會先走訪左子樹再走訪右子樹。 這樣一來每 pop 出一個 node 就是先走訪了父節點,接著就會再丟兩個 node 進入 stack,所以下一次的 pop 就代表先走訪左子樹的父節點,若左子樹不存在就會是走訪右子樹的父節點,這也就是 preorder traversal ***先走訪父節點,再走訪左子樹,再走訪右子樹*** 的作法。 ### 解法 3. Iterative Approach 2 Iterative 還有另一種作法,我們不用把每個左右邊 node 都存進 stack 中,只要把右邊 node 存進 stack 裡就好,而每次走訪左邊 node 時就會直接將其存到 `res` 的 list。 完整程式碼如下所示: ```cpp= /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> res; if (!root) return res; stack<TreeNode*> st; while (!st.empty() || root) { while (root) { res.push_back(root->val); if (root->right) st.push(root->right); root = root->left; } if (!st.empty()) { root = st.top(); st.pop(); } } return res; } }; ``` 程式碼的第 20 - 25 行,會一直走訪左邊 node 直到左邊 node 不存在為止,每一次的走訪就會把 node 存到 `res` 代表走訪父節點,再走訪左子樹,並同時把右邊 node 放到 stack,當左邊 node 走訪完之後,就會改走訪右邊 node,第 26 - 28 行就會把右邊 node pop 出來之後再繼續走訪父節點,再走訪左子樹的步驟。 ### 複雜度分析 時間複雜度分析上,不論是 recursive 還是 iterative 的作法都是要走訪所有的 nodes,所以時間複雜度為 $O(N)$。 空間複雜度分析上,recursive 的方法需要 stack 來存 `traversal` 函式讓之後可以跳回前一個 `traversal` 函式,當 tree 都偏向左或右的時候會有最差的空間複雜度 $O(N)$。因為若所有的 nodes 都是在左邊的話,則每次進入 `traversal` 函式遇到 `traversal(node->left)` 就會把當前 `traversal` 函式存入 stack 在繼續呼叫下一個 `traversal`。而正因為全部 nodes 都在左邊,stack 會存所有 nodes 數的 `traversal` 函式,中途不會被 pop 掉。 若不是在這種全部偏左或右的極端情況下,平均情況下空間複雜度為 $O(\log N)$。 而 iterative 的作法空間複雜度都只需要 $O(H)$,$H$ 為 tree 的高。 為什麼呢? 第一個 iterative 的作法,每一次從 stack pop node 就會最多在 push 兩個 node。在第一層 root 的時候 stack 只會存 root 所以只有一個,root pop 出來後會把它的左右 nodes 都 push 進 stack,stack 內就會存 2 個 nodes,而這也代表這個 tree 高有兩層。當我們在 pop 左邊的 node 時會再把它的下一層的左右 nodes push 進 stack 而這時 stack 有 3 個 nodes,tree 高也有三層。 第二個 iterative 的作法,stack 只會在每次走訪左邊 node 時存右邊 node,所以只要能夠一直往左邊走訪就會需要一直存右邊 node,而要存的右邊 node 數量就是看左邊走訪能到多深,這也就是 tree 的高。 :::info 學會了 preoder traversal,另外還有 inorder traversal 跟 postorder traversal 哦! 請看 [94. Binary Tree Inorder Traversal](https://hackmd.io/@Ji0m0/HyAgG6bU_) 及 [145. Binary Tree Postorder Traversal](https://hackmd.io/@Ji0m0/rybdMTbLO) :::