--- tags: 資料結構, LeetCode disqus: HackMD --- # 樹(Tree) :::spoiler 文章目錄 [toc] ::: ## 介紹 在計算機科學中,樹是一種用來組織和儲存資料的抽象資料結構。樹是由節點和連接它們的邊構成的,其中一個節點被稱為根節點,它沒有父節點。其它節點可以有一個或多個父節點和零個或多個子節點。 樹是一種分層資料結構,因為它的節點在不同的層級上。從根節點開始,可以透過沿著邊向下移動來造訪樹中的每個節點。每個節點可以有任意數量的子節點,但每個子節點只能有一個父節點。  在資料結構中,樹通常是**二元搜尋樹**,每個節點最多有兩個子節點,分別稱為**左子節點**和**右子節點**。二元搜尋樹是最常用的樹之一,因為它們易於實作且非常高效。二元搜尋樹可以用於實現搜尋樹、`Heap`、霍夫曼編碼(Huffman Coding)等資料結構。  (此為二元搜尋樹`Binary Search tree`) ### 定義 * 樹是一個沒有`loop`的`Graph`,所以下圖不是一個樹  * 樹必須有一個,也只能有一個根`root` ## 真實應用 * 文件系統中的文件夾 * 電子郵件信箱中的文件夾 * `HTML`文檔中的`DOM(Document Object Model)`樹 * 自然語言處理 下圖為`DOM`樹範例  (圖片來自於[[資料結構] 樹 Tree](https://w3c.hexschool.com/blog/bc00a810)) ## 建立樹的基本要點 1. **確定樹的根節點**:樹的根節點是整棵樹的開始,通常是從根節點開始進行操作。如果樹是空的,則根節點可以是`null`。 1. **定義節點的結構**:確定樹的節點結構,包括每個節點的值和指向其子節點的指標。可以使用 struct 或 class 來定義節點的結構。 1. **建立節點**:使用節點結構定義,建立每個節點。可以透過分配記憶體(如 `malloc()`、`new`)或從 object pool 中獲取節點來建立節點。 1. **建立連接關係**:通過將父節點的指標指向其子節點,來建立樹中節點之間的連接關係。可以使用遞迴或迭代演算法來建立連接關係,具體取決於應用程式的需求。 1. **走訪樹**:走訪樹是指按照一定的順序造訪樹的所有節點。常見的走訪方式包括前序走訪、中序走訪和後序走訪,也可以使用層序走訪來造訪樹的所有節點。 1. **處理樹的操作**:對樹進行查找、插入、刪除等操作。可以使用遞迴或迭代演算法來實作這些操作,具體取決於應用程式的需求。 ## 實作基本的樹 撰寫一個JS程式碼範例,該範例會產生樹,並且該樹的結構類似於下面提供的圖片。  [圖片使用syntree生成](https://mshang.ca/syntree/) 在這個範例中,我們建立了一個 `TreeNode` 類別,每個節點有一個 `value` 屬性代表該節點的值,以及一個 `children` 屬性代表該節點的子節點。 ```javascript= class TreeNode { constructor(value) { this.value = value; this.children = []; } addChild(node) { this.children.push(node); } } const root = new TreeNode(1); const node2 = new TreeNode(2); const node3 = new TreeNode(3); const node4 = new TreeNode(4); const node5 = new TreeNode(5); root.addChild(node2); root.addChild(node3); node2.addChild(node4); node2.addChild(node5); console.log(root); ``` ## 實作二元搜尋樹 以下程式碼是一個二分搜尋樹(Binary Search Tree, BST)的實作,包含三個部分: 1. `TreeNode` 類別:代表樹中的一個節點,包含一個值、一個左子節點和一個右子節點。 1. `BinarySearchTree` 類別:代表一個二分搜尋樹,包含一個根節點和一些操作方法,如插入、尋找和刪除節點。 1. 操作方法: * `insert(value)`:插入一個值為 `value` 的節點到樹中,如果樹為空則新節點設為根節點。如果值已經存在於樹中,則返回 `undefined`。 下圖為範例所產生二元搜尋樹  * `find(value)`:在樹中尋找值為 `value` 的節點,如果找到則返回該節點,否則返回 `false`。  * `remove(value)`:從樹中刪除值為 `value` 的節點。如果值不存在於樹中,則返回 `false`。  ```javascript= class BinarySearchTree { constructor() { this.root = null; } insert(value) { const newNode = new TreeNode(value); if (this.root === null) { this.root = newNode; return this; } else { let current = this.root; while (true) { if (value === current.value) return undefined; if (value < current.value) { if (current.left === null) { current.left = newNode; return this; } else { current = current.left; } } else if (value > current.value) { if (current.right === null) { current.right = newNode; return this; } else { current = current.right; } } } } } find(value) { if (this.root === null) return false; let current = this.root; let found = false; while (current && !found) { if (value < current.value) { current = current.left; } else if (value > current.value) { current = current.right; } else { found = true; } } if (!found) return false; return current; } remove(value) { if (this.root === null) return false; let current = this.root; let parent = null; let found = false; while (current && !found) { if (value < current.value) { parent = current; current = current.left; } else if (value > current.value) { parent = current; current = current.right; } else { found = true; } } if (!found) return false; if (current.left === null && current.right === null) { if (current.value < parent.value) { parent.left = null; } else if (current.value > parent.value) { parent.right = null; } } else if (current.left === null) { if (current.value < parent.value) { parent.left = current.right; } else if (current.value > parent.value) { parent.right = current.right; } } else if (current.right === null) { if (current.value < parent.value) { parent.left = current.left; } else if (current.value > parent.value) { parent.right = current.left; } } else { let temp = current.right; while (temp.left !== null) { temp = temp.left; } current.value = temp.value; if (temp.right === null) { temp = null; } else { temp.value = temp.right.value; temp.right = null; } } } } const bst = new BinarySearchTree(); bst.insert(10); bst.insert(5); bst.insert(13); bst.insert(11); bst.insert(2); bst.insert(16); console.log(bst.find(11)); console.log(bst.remove(13)); console.log(bst); ``` ## 樹的走訪Tree Traversal 樹的走訪大致分成下面兩項 * 深度優先搜尋`(deppth-first search)`簡稱`DFS` * 廣度優先搜尋`(breadth-first search)`簡稱`BFS`  (圖片來自於[DFS vs BFS (in detail)](https://iq.opengenus.org/dfs-vs-bfs/)) ### 深度優先搜尋 深度優先搜尋詳細解說請參考[👉深度優先搜尋](https://hackmd.io/@SupportCoding/DFS) 樹的走訪在深度優先搜尋中為以下三種方式,分別代表根節點在走訪時的位置: 1. 前序走訪 (Pre-Order Traversal) 2. 中序走訪 (In-Order Traversal) 3. 後序走訪 (Post-Order Traversal) 接下來再個別探討的過程中,皆以**二元搜尋樹**做為範例。 #### 前序走訪 (Pre-Order Traversal) > 根節點排在前面 前序走訪`(Pre-Order Traversal)`是依序以**根節點、左節點、右節點**為順序走訪的方式。  #### LeetCode實戰 [144. Binary Tree Preorder Traversal](https://leetcode.com/problems/binary-tree-preorder-traversal) 題目: 給定一個二元搜尋樹,傳回其前序走訪的值。  (圖片來自於[Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page)) #### Code [👉詳細說明](https://leetcode.com/problems/binary-tree-preorder-traversal/solutions/3033034/javascript-solutions-detail/?envType=study-plan&id=data-structure-i&orderBy=most_votes) 前序走訪是**根->左->右**,我們先用一個陣列儲存要輸出的值,檢查節點是否為空,否的話則將值儲存至陣列,並利用遞迴的方式持續檢查左右子節點,最後再傳回該陣列即可。 ```javascript= /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {number[]} */ var preorderTraversal = function(root) { var target = [] function dfs(node){ if (!node){ return; } target.push(node.val) dfs(node.left) dfs(node.right) } dfs(root) return target }; ``` #### 中序走訪 (In-Order Traversal) > 根節點排在中間 中序走訪`(In-Order Traversal)`是依序以**左節點、根節點、右節點**為順序走訪的方式。  (圖片來自於[Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page)) #### LeetCode實戰 [94. Binary Tree Inorder Traversal](https://leetcode.com/problems/binary-tree-inorder-traversal) 題目: 給定一個二元搜尋樹,傳回其中序走訪的值。  #### Code [👉詳細說明](https://leetcode.com/problems/binary-tree-inorder-traversal/solutions/3038824/javascript-solutions-detail/?envType=study-plan&id=data-structure-i&orderBy=most_votes) 我們使用一個堆疊`(stack)`來存儲遍歷過的節點,並使用一個變量 `curr` 來記錄當前訪問的節點。 當 `curr` 為空時,從堆疊中彈出一個節點 `curr`,將其值存儲到 `target` 陣列中,然後將 `curr` 設置為其右子節點。因為在中序走訪中,左子節點必須先於根節點被處理,所以我們先處理完左子節點,再處理根節點,最後再處理右子節點。這樣保證了按照中序走訪的順序進行訪問。 ```javascript= /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {number[]} */ var inorderTraversal = function(root) { let target = [] let stack = [] let curr = root while (curr != null || !stack.length == 0) { while (curr != null) { stack.push(curr); curr = curr.left; } curr = stack.pop(); target.push(curr.val); curr = curr.right; } return target; }; ``` #### 後序走訪 (Post-Order Traversal) > 根節點排在最後 後序走訪`(Post-Order Traversal)`是依序以**左節點、右節點、根節點**為順序走訪的方式。  題目: 給定一個二元搜尋樹,傳回其後序走訪的值。 #### LeetCode實戰 [145. Binary Tree Postorder Traversal](https://leetcode.com/problems/binary-tree-postorder-traversal) 題目: 給定一個二元搜尋樹,傳回其後序走訪的值。  #### Code [👉詳細說明](https://leetcode.com/problems/binary-tree-postorder-traversal/solutions/3044353/javascript-simple-solution/?envType=study-plan&id=data-structure-i&orderBy=most_votes) 這邊解法有點像是把它反推回去,將節點往前加入陣列。 1. 在 `while` 迴圈中,將 `current` 的值加入結果陣列 `result` 的開頭,此為後序遍歷的特點。 1. 將 `current` 加入堆疊 `stack` 中。 1. 將 `current` 更新為其右子節點,一路向右子樹走,直到無法再往右。 1. 當 `current` 為空時,表示已經走到了右子樹的底部,此時從堆疊 `stack` 中彈出一個節點,將 `current` 更新為該節點的左子節點,以便繼續遍歷左子樹。因為左子樹已經遍歷完畢,所以這裡不需要將該節點加入結果陣列 `result` 中。 1. 重複執行步驟 `1-4`,直到 `current` 為空且堆疊 `stack` 也為空,表示已經遍歷完整棵樹。 最後,回傳遍歷結果陣列 `result`。 ```javascript= /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {number[]} */ var postorderTraversal = function(root) { let result = []; let stack = []; let current = root; while(current || stack.length) { while(current) { result.unshift(current.val); stack.push(current); current = current.right; } current = stack.pop(); current = current.left; } return result; }; ``` ### 廣度優先搜尋 廣度優先搜尋詳細解說請參考[👉廣度優先搜尋](https://hackmd.io/@SupportCoding/BFS) 樹的走訪在廣度優先搜尋中的方式: * 層序走訪 #### 層序走訪 層序走訪會先造訪離根節點最近的節點,並由左至右存取。  #### LeetCode實戰 [102. Binary Tree Level Order Traversal](https://leetcode.com/problems/binary-tree-level-order-traversal/) 題目: 給定一個二元搜尋樹,傳回其層序走訪的值。  #### Code [👉詳細說明](https://leetcode.com/problems/binary-tree-level-order-traversal/solutions/3062350/javascript-solutions-detail/?orderBy=most_votes) 1. 判斷樹是否為空,如果是,返回一個空陣列。 1. 宣告一個 `queue` 陣列,並將根節點存入 `queue`。 1. 宣告一個 `result` 陣列。 1. 開始進行 `while` 迴圈,當 `queue` 不為空時,進入迴圈。 1. 在迴圈中,宣告一個 `level` 陣列,代表當前層級的節點。 1. 計算當前層級節點的數量,使用 `size` 變數存儲。 1. 使用 `for` 迴圈,從 `queue` 中取出 `size` 個節點進行處理,直到當前層級所有節點處理完成。 1. 將當前節點的值存入 `level` 陣列中。 1. 如果當前節點有左子節點,將其加入 `queue` 中。 1. 如果當前節點有右子節點,將其加入 `queue` 中。 1. 將 `level` 陣列存入 `result` 二維陣列中。 1. 迴圈結束後,返回 `result` 陣列。 ```javascript= /** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {number[][]} */ var levelOrder = function(root) { if (root === null) return []; let queue = [root]; let result = []; while (queue.length > 0) { let level = []; let size = queue.length; for (let i = 0; i < size; i++) { let node = queue.shift(); level.push(node.val); if (node.left !== null) queue.push(node.left); if (node.right !== null) queue.push(node.right); } result.push(level); } return result; }; ``` ## 特別感謝 感謝[Howard Gou](https://hackmd.io/@toto6038)大大訂正
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up