|
| 1 | + |
| 2 | +# Understanding Trees, Binary Search Trees, and Finding the Lowest Common Ancestor |
| 3 | + |
| 4 | +In the world of computer science, data structures are the building blocks of efficient algorithms. One of the most fundamental and versatile data structures is the **Tree**. |
| 5 | + |
| 6 | +This post will take you on a journey from the basics of trees to a specific type called a Binary Search Tree (BST), explore common algorithms used with them, and finally, solve a classic problem: finding the Lowest Common Ancestor of two nodes in a BST. |
| 7 | + |
| 8 | +## What is a Tree? |
| 9 | + |
| 10 | +In computer science, a **Tree** is a hierarchical data structure that consists of nodes connected by edges. |
| 11 | + |
| 12 | +Unlike linear data structures like arrays or linked lists, trees are non-linear and are used to represent hierarchical relationships. |
| 13 | + |
| 14 | +### Key Terminology: |
| 15 | + |
| 16 | +* **Node:** The fundamental part of a tree that stores data. |
| 17 | +* **Edge:** The connection between two nodes. |
| 18 | +* **Root:** The topmost node in a tree. It's the only node that doesn't have a parent. |
| 19 | +* **Parent:** A node that has a child node. |
| 20 | +* **Child:** A node that has a parent node. |
| 21 | +* **Leaf:** A node that does not have any children. |
| 22 | +* **Subtree:** A tree consisting of a node and its descendants. |
| 23 | +* **Depth:** The length of the path from the root to a specific node. |
| 24 | +* **Height:** The length of the longest path from a specific node to a leaf. |
| 25 | + |
| 26 | +Trees are used in various applications, such as file systems, organization charts, and even in parsing expressions in compilers. |
| 27 | + |
| 28 | +## Binary Search Trees (BSTs) |
| 29 | + |
| 30 | +A **Binary Search Tree (BST)** is a special type of binary tree where the nodes are ordered in a specific way. |
| 31 | + |
| 32 | +This ordering makes operations like searching, insertion, and deletion very efficient. |
| 33 | + |
| 34 | +A binary tree is a BST if it satisfies the following properties: |
| 35 | + |
| 36 | +1. The left subtree of a node contains only nodes with keys **lesser** than the node's key. |
| 37 | +2. The right subtree of a node contains only nodes with keys **greater** than the node's key. |
| 38 | +3. Both the left and right subtrees must also be binary search trees. |
| 39 | + |
| 40 | +This structure ensures that for any given node, all the values in its left subtree are smaller, and all the values in its right subtree are larger. |
| 41 | + |
| 42 | +## Common Tree Algorithms |
| 43 | + |
| 44 | +Trees have a variety of algorithms for traversal and manipulation. The most common are traversal algorithms, which visit each node in the tree exactly once. |
| 45 | + |
| 46 | +### Tree Traversal |
| 47 | + |
| 48 | +There are two main approaches to traversing a tree: |
| 49 | + |
| 50 | +1. **Depth-First Search (DFS):** This approach explores as far as possible down one branch before backtracking. There are three common ways to perform DFS: |
| 51 | + * **In-order Traversal:** Visit the left subtree, then the root, then the right subtree. For a BST, this traversal visits the nodes in ascending order. |
| 52 | + * **Pre-order Traversal:** Visit the root, then the left subtree, then the right subtree. This is useful for creating a copy of the tree. |
| 53 | + * **Post-order Traversal:** Visit the left subtree, then the right subtree, then the root. This is useful for deleting nodes from the tree. |
| 54 | + |
| 55 | +2. **Breadth-First Search (BFS):** This approach explores the tree level by level. It visits all the nodes at a given depth before moving on to the next level. BFS is typically implemented using a queue. |
| 56 | + |
| 57 | +## More Tree Algorithms in Go |
| 58 | + |
| 59 | +Let's explore how to implement some of these fundamental tree algorithms in Go. |
| 60 | + |
| 61 | +### Finding the Height/Depth of a Binary Tree |
| 62 | + |
| 63 | +The **height** of a binary tree is the number of edges on the longest path from the root node to a leaf node. A tree with only a root node has a height of 0. |
| 64 | + |
| 65 | +The concept is closely related to the **depth** of a node, which is its distance from the root. The height of the tree is, therefore, the maximum depth of any node in the tree. |
| 66 | + |
| 67 | +We can calculate the height recursively. The height of a node is 1 plus the maximum height of its left or right subtree. |
| 68 | + |
| 69 | +```go |
| 70 | +import "math" |
| 71 | + |
| 72 | +// TreeNode definition from before |
| 73 | +type TreeNode struct { |
| 74 | + Val int |
| 75 | + Left *TreeNode |
| 76 | + Right *TreeNode |
| 77 | +} |
| 78 | + |
| 79 | +func max(a, b int) int { |
| 80 | + if a > b { |
| 81 | + return a |
| 82 | + } |
| 83 | + return b |
| 84 | +} |
| 85 | + |
| 86 | +func height(node *TreeNode) int { |
| 87 | + if node == nil { |
| 88 | + return -1 // Height of a null tree is -1 |
| 89 | + } |
| 90 | + leftHeight := height(node.Left) |
| 91 | + rightHeight := height(node.Right) |
| 92 | + return 1 + max(leftHeight, rightHeight) |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +### DFS Traversals in Go |
| 97 | + |
| 98 | +Here are the Go implementations for the three DFS traversal methods. |
| 99 | + |
| 100 | +#### In-order Traversal |
| 101 | + |
| 102 | +```go |
| 103 | +func inOrderTraversal(node *TreeNode) { |
| 104 | + if node == nil { |
| 105 | + return |
| 106 | + } |
| 107 | + inOrderTraversal(node.Left) |
| 108 | + fmt.Println(node.Val) // Process the node |
| 109 | + inOrderTraversal(node.Right) |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +#### Pre-order Traversal |
| 114 | + |
| 115 | +```go |
| 116 | +func preOrderTraversal(node *TreeNode) { |
| 117 | + if node == nil { |
| 118 | + return |
| 119 | + } |
| 120 | + fmt.Println(node.Val) // Process the node |
| 121 | + preOrderTraversal(node.Left) |
| 122 | + preOrderTraversal(node.Right) |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +#### Post-order Traversal |
| 127 | + |
| 128 | +```go |
| 129 | +func postOrderTraversal(node *TreeNode) { |
| 130 | + if node == nil { |
| 131 | + return |
| 132 | + } |
| 133 | + postOrderTraversal(node.Left) |
| 134 | + postOrderTraversal(node.Right) |
| 135 | + fmt.Println(node.Val) // Process the node |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +## LeetCode 235: Lowest Common Ancestor of a Binary Search Tree |
| 140 | + |
| 141 | +Now, let's apply our knowledge to a classic problem. |
| 142 | + |
| 143 | +The **Lowest Common Ancestor (LCA)** of two nodes, `p` and `q`, in a tree is the lowest (i.e., deepest) node that has both `p` and `q` as descendants. |
| 144 | + |
| 145 | +### The Problem |
| 146 | + |
| 147 | +Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BST. |
| 148 | + |
| 149 | +For example, consider the following BST: |
| 150 | + |
| 151 | +``` |
| 152 | + 6 |
| 153 | + / \ |
| 154 | + 2 8 |
| 155 | + / \ / \ |
| 156 | + 0 4 7 9 |
| 157 | + / \ |
| 158 | + 3 5 |
| 159 | +``` |
| 160 | + |
| 161 | +* The LCA of nodes `2` and `8` is `6`. |
| 162 | +* The LCA of nodes `2` and `4` is `2`, since a node can be a descendant of itself. |
| 163 | +* The LCA of nodes `3` and `5` is `4`. |
| 164 | + |
| 165 | + |
| 166 | +### The Solution |
| 167 | + |
| 168 | +The properties of a BST make finding the LCA particularly efficient. |
| 169 | + |
| 170 | +We can start at the root of the tree and use the values of `p` and `q` to decide where to go next. |
| 171 | + |
| 172 | +Let's consider the current node we are at, let's call it `current`. |
| 173 | + |
| 174 | +1. If both `p` and `q` are **greater** than `current.val`, it means that the LCA must be in the **right** subtree. So, we move to the right child. |
| 175 | +2. If both `p` and `q` are **less** than `current.val`, it means that the LCA must be in the **left** subtree. So, we move to the left child. |
| 176 | +3. If one of `p` or `q` is greater than `current.val` and the other is less than `current.val` (or if one of them is equal to `current.val`), then `current` is the LCA. |
| 177 | + |
| 178 | +This is because `p` and `q` are on opposite sides of the current node, meaning it's the split point and thus the lowest common ancestor. |
| 179 | + |
| 180 | +We can implement this logic both iteratively and recursively. |
| 181 | + |
| 182 | +### Iterative Solution |
| 183 | + |
| 184 | +```go |
| 185 | +/** |
| 186 | + * Definition for a binary tree node. |
| 187 | + * type TreeNode struct { |
| 188 | + * Val int |
| 189 | + * Left *TreeNode |
| 190 | + * Right *TreeNode |
| 191 | + * } |
| 192 | + */ |
| 193 | + |
| 194 | +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { |
| 195 | + current := root |
| 196 | + for current != nil { |
| 197 | + if p.Val > current.Val && q.Val > current.Val { |
| 198 | + current = current.Right |
| 199 | + } else if p.Val < current.Val && q.Val < current.Val { |
| 200 | + current = current.Left |
| 201 | + } else { |
| 202 | + return current |
| 203 | + } |
| 204 | + } |
| 205 | + return nil // Should not be reached in a valid BST |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +### Recursive Solution |
| 210 | + |
| 211 | +```go |
| 212 | +/** |
| 213 | + * Definition for a binary tree node. |
| 214 | + * type TreeNode struct { |
| 215 | + * Val int |
| 216 | + * Left *TreeNode |
| 217 | + * Right *TreeNode |
| 218 | + * } |
| 219 | + */ |
| 220 | + |
| 221 | +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { |
| 222 | + if root == nil { |
| 223 | + return nil |
| 224 | + } |
| 225 | + |
| 226 | + if p.Val > root.Val && q.Val > root.Val { |
| 227 | + return lowestCommonAncestor(root.Right, p, q) |
| 228 | + } else if p.Val < root.Val && q.Val < root.Val { |
| 229 | + return lowestCommonAncestor(root.Left, p, q) |
| 230 | + } else { |
| 231 | + return root |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +Both of these solutions are very efficient, with a time complexity of O(H), where H is the height of the tree. In a balanced BST, this is O(log N), where N is the number of nodes. |
| 237 | + |
| 238 | +The space complexity for the iterative solution is O(1), while the recursive solution has a space complexity of O(H) due to the call stack. |
| 239 | + |
| 240 | +## Conclusion |
| 241 | + |
| 242 | +Trees and Binary Search Trees are powerful data structures that are essential for any programmer's toolkit. By understanding their properties and the algorithms that operate on them, you can solve a wide range of problems efficiently. |
| 243 | + |
| 244 | +The Lowest Common Ancestor problem is a perfect example of how the structure of a BST can be leveraged to find an elegant and optimal solution. |
0 commit comments