4.24Trie
4.25Segment Tree
Each solves a different class of problems.
4.8.48Tree vs Linked List
- A linked list is essentially a restricted tree-like structure where nodes normally have at most one next direction.
- Linked List:
- 10 → 20 → 30 → 40
- A tree branches:
- 10
/ \
20 30
/ \ \
40 50 60
The major advantage of trees is that they can represent hierarchical relationships and, in specialized forms, support efficient searching.
4.8.49Key Takeaways
- The most important tree terminology is:
- Root
- Parent
- Child
- Sibling
- Leaf
- Internal Node
- Edge
- Path
- Depth
- Height
- Subtree
- Ancestor
- Descendant
- Degree
- The major traversal methods are:
Preorder → Node → Left → Right
Inorder → Left → Node → Right
Postorder → Left → Right → Node
- Level Order → Level by Level
- The most important structural types are:
- General Tree
- Binary Tree
- Full Binary Tree
- Complete Binary Tree
- Perfect Binary Tree
- Balanced Tree
- Degenerate Tree
- And remember these fundamental properties:
Tree with n nodes
▼
n - 1 edges
Tree
▼
Connected
▼
No cycles
Finally, the most important algorithms and patterns to master are:
DFS, BFS, recursion, tree height, tree diameter, lowest common ancestor, subtree processing, and top-down/bottom-up recursion.
The next section, 4.9 Binary Trees, builds directly on these concepts and introduces the two-child left/right structure that forms the foundation for BSTs, heaps, expression trees, and many tree interview problems.
4.9Binary Trees
4.9Binary Trees
- A Binary Tree is a tree data structure in which each node has at most two children.
- The two children are called:
- Left child
- Right child
- A binary tree can look like:
- 10
/ \
5 20
/ \ \
3 7 30
- Each node can have:
- 0 children
- 1 child
- 2 children
- but never more than two.
4.9.1Binary Tree Node
A binary-tree node normally contains three components:
- Node
- ├── Data
- ├── Left
- └── Right
- In Python:
class TreeNode:
def __init__(self, value):
self.value = value- self.left = None
- self.right = None
- Create a simple binary tree:
- root = TreeNode(10)
- root.left = TreeNode(5)
- root.right = TreeNode(20)
- The structure is:
- 10
/ \
5 20
4.9.2Building a Binary Tree
Let's create a larger tree:
- root = TreeNode(10)
- root.left = TreeNode(5)
- root.right = TreeNode(20)
- root.left.left = TreeNode(3)
- root.left.right = TreeNode(7)
- root.right.right = TreeNode(30)
- The resulting tree:
- 10
/ \
5 20
/ \ \
3 7 30
Notice that the tree does not necessarily have to be sorted.
That distinction becomes important when we study Binary Search Trees.
4.9.3Binary Tree Terminology
Consider:
A
/ \
B C
/ \ \
D E F
- Root
- A
- is the root.
- Children
B and C are children of A.
- Parent
- A is the parent of B and C.
- Siblings
- B and C are siblings.
- D and E are siblings.
- Leaf Nodes
- D, E, F
- are leaf nodes.
- Internal Nodes
- A, B, C
- are internal nodes.
4.9.4Binary Tree Properties
For a binary tree:
Each node can have at most two children.
Therefore, at level 0:
- Maximum nodes = 1
- At level 1:
- Maximum nodes = 2
- At level 2:
- Maximum nodes = 4
- At level 3:
- Maximum nodes = 8
In general, the maximum number of nodes at level L is:
2^L
when the root is at level 0.
4.9.5Maximum Number of Nodes
For a binary tree of height h, where the root has height 0, the maximum number of nodes is:
2^(h + 1) - 1
For example, height 2:
A
/ \
B C
/ \ / \
D E F G
Number of nodes:
2^(2 + 1) - 1
= 8 - 1
= 7
4.9.6Minimum Height
- A binary tree containing n nodes can have a minimum height when it is as balanced as possible.
- Approximately:
- h = O(log n)
This is why balanced binary trees can support efficient operations.
4.9.7Maximum Height
The maximum height occurs when every node has only one child.
Example:
10
\
20
\
30
\
40
\
- 50
- If there are n nodes:
- height = n - 1
Therefore:
Minimum height → O(log n)
Maximum height → O(n)
4.9.8Full Binary Tree
A Full Binary Tree is a binary tree where every node has either:
- 0 children
- or:
- 2 children
Example:
1
/ \
2 3
/ \
4 5
- Node 2 has two children.
- Node 3 has zero children.
- Node 4 and 5 have zero children.
- There are no nodes with exactly one child.
4.9.9Complete Binary Tree
- A Complete Binary Tree has:
- Every level completely filled except possibly the last.
- The last level filled from left to right.
Example:
1
/ \
2 3
/ \ /
4 5 6
This is complete.
But:
1
/ \
2 3
/ \
4 6
is not complete because the last level is not filled from left to right.
Complete binary trees are especially important for heaps.
4.9.10Perfect Binary Tree
- A Perfect Binary Tree has:
- Every internal node with exactly two children.
- All leaf nodes at the same level.
Example:
1
/ \
2 3
/ \ / \
4 5 6 7
For height h:
Nodes = 2^(h + 1) - 1
4.9.11Balanced Binary Tree
A balanced binary tree has subtrees whose heights remain reasonably close.
Example:
10
/ \
5 20
/ \ / \
3 7 15 25
- A balanced tree generally has:
- Height = O(log n)
- Balanced trees are important for efficient searching.
4.9.12Degenerate Binary Tree
A degenerate binary tree has essentially one child at each level.
Example:
10
\
20
\
30
\
- 40
- It behaves almost like a linked list.
- For n nodes:
- Height = O(n)
4.9.13Skewed Binary Tree
- A skewed tree can lean entirely to one side.
- Left-skewed
- 40
/
30
/
20
/
\
20
\
30
\
40
Both have height:
O(n)
4.9.14Binary Tree Traversals
The four major traversal techniques are:
- 1. Preorder
- 2. Inorder
- 3. Postorder
- 4. Level Order
Consider:
1
/ \
2 3
/ \
4 5
4.9.15Preorder Traversal
Preorder follows:
For:
1
/ \
2 3
/ \
4 5
- the result is:
- 1 2 4 5 3
- Implementation:
def preorder(root):
if root is None:
returnprint(root.value)
- preorder(root.left)
- preorder(root.right)
- Complexity:
Time = O(n)
Space = O(h)
where h is the tree height.
4.9.16Inorder Traversal
Inorder follows:
For:
1
/ \
2 3
/ \
4 5
- the result is:
- 4 2 5 1 3
- Implementation:
def inorder(root):
if root is None:
returninorder(root.left)
print(root.value)
inorder(root.right)
Inorder traversal becomes especially important for Binary Search Trees, where it produces values in sorted order.
4.9.17Postorder Traversal
Postorder follows:
For:
1
/ \
2 3
/ \
4 5
- the result is:
- 4 5 2 3 1
- Implementation:
def postorder(root):
if root is None:
returnpostorder(root.left)
postorder(root.right)
print(root.value)
Postorder is useful when children must be processed before their parent.
4.9.18Level-Order Traversal
Level-order visits nodes level by level.
Example:
1
/ \
2 3
/ \
4 5
- Result:
- 1 2 3 4 5
- Implementation:
from collections import deque
def level_order(root):
if root is None:
return []
result = []
queue = deque([root])
while queue:
node = queue.popleft()
result.append(node.value)
if node.left:
queue.append(node.left)if node.right:
queue.append(node.right)return result
Complexity:
Time = O(n)
Space = O(w)
where w is the maximum width of the tree.
4.9.19Traversal Summary
For:
1
/ \
2 3
/ \
4 5
| Traversal | Result |
|---|
| Preorder | 1 2 4 5 3 |
| Inorder | 4 2 5 1 3 |
| Postorder | 4 5 2 3 1 |
| Level Order | 1 2 3 4 5 |
- A useful memory trick:
- PRE:
- Node first
- IN:
- Node in the middle
- POST:
- Node last
4.9.20Iterative Preorder
Preorder can be implemented without recursion using a stack.
def preorder_iterative(root):
if root is None:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.value)
if node.right:
stack.append(node.right)if node.left:
stack.append(node.left)return result
- Why add the right child first?
- Because the stack is LIFO.
- We want the left child to be processed first.
4.9.21Iterative Inorder
Inorder can also be implemented using a stack.
def inorder_iterative(root):
result = []
stack = []
current = root
while current is not None or stack:
while current is not None:
stack.append(current)current = current.left
current = stack.pop()
result.append(current.value)
current = current.right
return result
This is a very important interview pattern.
4.9.22Iterative Postorder
Postorder can be implemented using stacks as well.
A simple two-stack approach:
def postorder_iterative(root):
if root is None:
return []
stack1 = [root]
stack2 = []
result = []
while stack1:
node = stack1.pop()
stack2.append(node)
if node.left:
stack1.append(node.left)if node.right:
stack1.append(node.right)while stack2:
result.append(stack2.pop().value)return result
There are also optimized one-stack approaches.
4.9.23Finding Maximum Depth
The maximum depth is the longest distance from the root to a leaf.
def max_depth(root):
if root is None:
return 0
left_depth = max_depth(root.left)
right_depth = max_depth(root.right)
return 1 + max(left_depth, right_depth)
Complexity:
Time = O(n)
- Space = O(h)
- For a balanced tree:
- h = O(log n)
- For a skewed tree:
h = O(n)
4.9.24Minimum Depth
Minimum depth is the shortest path from the root to a leaf.
def min_depth(root):
if root is None:
return 0
if root.left is None:
return 1 + min_depth(root.right)
if root.right is None:
return 1 + min_depth(root.left)
return 1 + min(
min_depth(root.left),
min_depth(root.right)
)For shortest-depth problems, BFS can often be especially natural because it processes nodes level by level.
4.9.25Count Nodes
def count_nodes(root):
if root is None:
return 0
return (
1
+ count_nodes(root.left)
+ count_nodes(root.right)
)- Complexity:
- Time = O(n)
- Every node is visited.
4.9.26Sum of Nodes
def sum_tree(root):
if root is None:
return 0
return (
root.value
+ sum_tree(root.left)
+ sum_tree(root.right)
)For:
10
/ \
5 20
O(n)
4.9.27Find Maximum Value
def maximum(root):
if root is None:
return float("-inf")
return max(
root.value,
maximum(root.left),
maximum(root.right)
)For a normal binary tree, every node may need to be examined.
Therefore:
Time = O(n)
Do not confuse this with a Binary Search Tree, where ordering provides additional information.
4.9.28Search in a Binary Tree
Because a normal binary tree has no ordering rule, we may need to search the entire tree.
def search(root, target):
if root is None:
return False
if root.value == target:
return True
return (
search(root.left, target)
or search(root.right, target)
)Worst case:
O(n)
4.9.29Invert a Binary Tree
- A famous interview problem is to invert or mirror a binary tree.
- Original:
- 1
/ \
2 3
/ \
4 5
After inversion:
1
/ \
3 2
/ \
5 4
Implementation:
def invert_tree(root):
if root is None:
return None
- root.left, root.right = root.right, root.left
- invert_tree(root.left)
- invert_tree(root.right)
return root
Complexity:
Time = O(n)
Space = O(h)
This is a classic example of recursive tree processing.
4.9.30Check if Two Trees Are Identical
- Given two trees:
- Tree A:
- 1
/ \
2 3
/ \
2 3
- they are identical if:
- Their corresponding values match.
- Their left subtrees match.
- Their right subtrees match.
def same_tree(a, b):
if a is None and b is None:
return True
if a is None or b is None:
return False
return (
a.value == b.value
and same_tree(a.left, b.left)
and same_tree(a.right, b.right)
)Complexity:
Time = O(n)
4.9.31Check if a Tree Is Symmetric
A tree is symmetric if its left and right sides are mirror images.
Example:
1
/ \
2 2
/ \ / \
3 4 4 3
This is symmetric.
A recursive solution compares:
left.left ↔ right.right
left.right ↔ right.left
def is_mirror(left, right):
if left is None and right is None:
return True
if left is None or right is None:
return False
return (
left.value == right.value
and is_mirror(left.left, right.right)
and is_mirror(left.right, right.left)
)4.9.32Root-to-Leaf Path
A common problem asks whether there is a path from root to a leaf whose values sum to a target.
Example:
5
/ \
4 8
/ / \
11 7 4
/ \
2 1
- For target:
- 22
- one valid path is:
- 5 → 4 → 11 → 2
- because:
- 5 + 4 + 11 + 2 = 22
- Implementation:
def has_path_sum(root, target):
if root is None:
return False
if root.left is None and root.right is None:
return root.value == target
remaining = target - root.value
return (
has_path_sum(root.left, remaining)
or has_path_sum(root.right, remaining)
)- Complexity:
- Time = O(n)
- Space = O(h)
4.9.33Lowest Common Ancestor
Consider:
1
/ \
2 3
/ \
4 5
The LCA of 4 and 5 is:
2
For a general binary tree, a recursive approach is:
def lowest_common_ancestor(root, p, q):
if root is None:
return None
if root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
return root
return left if left else right
- Complexity:
- Time = O(n)
- Space = O(h)
4.9.34Tree Diameter
- The diameter is the longest path between two nodes.
- Consider:
- 1
/ \
2 3
/ \
4 5
- Longest path:
- 4 → 2 → 1 → 3
- Diameter:
- 3 edges
A common O(n) solution calculates subtree heights.
def diameter(root):
maximum = 0
def height(node):
nonlocal maximumif node is None:
return 0
left = height(node.left)
right = height(node.right)
maximum = max(maximum, left + right)
return 1 + max(left, right)
height(root)
return maximum
Time complexity:
O(n)
4.9.35Balanced Tree Check
We can check whether a binary tree is height-balanced.
A naive approach repeatedly calculates heights and may become O(n²).
A better bottom-up approach calculates height and balance information together.
def is_balanced(root):
def check(node):
if node is None:
return 0
left = check(node.left)
if left == -1:
return -1
right = check(node.right)
if right == -1:
return -1
if abs(left - right) > 1:
return -1
return 1 + max(left, right)
return check(root) != -1
Complexity:
Time = O(n)
Space = O(h)
This is an excellent example of bottom-up tree recursion.
4.9.36Zigzag Level-Order Traversal
- Normal level order:
- 1
- 2 3
- 4 5 6 7
- Zigzag traversal:
- 1
- 3 2
- 4 5 6 7
- The direction alternates at every level.
- A deque can be useful for this problem.
- This combines:
- Tree
+
BFS
+
Deque
4.9.37Left View of a Binary Tree
The left view contains the first visible node from each level.
Example:
1
/ \
2 3
/ \ \
4 5 6
Left view:
1 2 4
A BFS traversal can track the first node at each level.
4.9.38Right View of a Binary Tree
- The right view contains the last visible node from each level.
- For:
- 1
/ \
2 3
/ \ \
4 5 6
right view:
1 3 6
These problems are common in interviews and demonstrate level-order traversal.
4.9.39Boundary Traversal
Boundary traversal visits the outer boundary of a binary tree.
Conceptually:
Left Boundary
▼
Root
▼
Right Boundary
Leaf nodes are generally included carefully to avoid duplicates.
This is an advanced traversal problem.
4.9.40Serialize and Deserialize
Serialization converts a tree into a storable/transmittable representation.
Example:
1
/ \
2 3
- could be represented as:
- "1,2,None,None,3,None,None"
- Deserialization reconstructs the tree.
- This problem is important in:
- Data storage
- APIs
- Distributed systems
- Coding interviews
4.9.41Binary Tree and Stack
DFS traversal can be implemented using a stack.
Binary Tree
▼
Stack
▼
DFS
For preorder:
stack = [root]
while stack:
node = stack.pop()
if node.right:
stack.append(node.right)if node.left:
stack.append(node.left)- This demonstrates the connection between:
- Section 4.5 – Stacks
- and:
- Section 4.9 – Binary Trees
4.9.42Binary Tree and Queue
Level-order traversal uses a queue.
Binary Tree
▼
Queue
▼
BFS
- This connects:
- Section 4.6 – Queues
- with binary trees.
This relationship becomes extremely important in coding interviews.
4.9.43Binary Tree vs Binary Search Tree
These are not the same thing.
A Binary Tree only requires:
At most 2 children
There is no requirement that values be ordered.
Example:
10
/ \
50 2
This is a valid binary tree.
- But it is not a valid Binary Search Tree.
- A BST additionally requires an ordering rule.
- Typically:
- Left subtree < Node < Right subtree
- BSTs are covered in the next section.
4.9.44Binary Tree vs Heap
- A heap is also a specialized binary tree.
- A typical binary heap must satisfy:
- Complete binary tree structure.
- Heap-order property.
- For a min-heap:
- Parent ≤ Children
Example:
1
/ \
3 5
/ \
7 9
Heaps are covered in Section 4.11.
4.9.45Binary Trees in AI
Binary trees have several AI and ML applications.
Decision Trees
A decision can branch into two outcomes:
Age > 30?
/ \
Yes No
/ \
Approve Review
- Game Trees
- Game-playing AI can represent possible moves:
- Current State
/ \
Move A Move B
/ \ / \
State State State State
- Algorithms such as Minimax use tree structures.
- Search Problems
- Trees can represent possible states and decisions.
4.9.46Binary Tree Complexity
For a binary tree containing n nodes:
| Operation | Complexity |
|---|
| Traversal | O(n) |
| Search | O(n) |
| Count nodes | O(n) |
| Find height | O(n) |
| Find maximum | O(n) |
| Invert tree | O(n) |
| Check symmetry | O(n) |
| Check balance | O(n) |
| Diameter | O(n) |
Auxiliary recursion space:
O(h)
where h is tree height.
Therefore:
Balanced tree → O(log n)
Skewed tree → O(n)
4.9.47Important Binary Tree Patterns
Pattern 1 — DFS Recursion
Process Node
▼
Process Left
▼
Process Right
- Used for:
- Traversals
- Height
- Path sums
- Diameter
- Pattern 2 — BFS
- Used for:
- Level order
- Minimum depth
- Tree views
- Zigzag traversal
- Pattern 3 — Bottom-Up
- Left Result
+
Right Result
▼
Node Result
- Used for:
- Height
- Diameter
- Balance
- Maximum path sum
- Pattern 4 — Top-Down
Parent Information
▼
Child
▼
Grandchild
- Used for:
- Path problems
- Depth
- Root-to-leaf constraints
4.9.48Common Binary Tree Interview Problems
Beginner
- Preorder traversal
- Inorder traversal
- Postorder traversal
- Level-order traversal
- Maximum depth
- Minimum depth
- Count nodes
- Count leaves
- Search for a value
- Sum of nodes
Intermediate
- Invert binary tree
- Check identical trees
- Check symmetric tree
- Check balanced tree
- Diameter of binary tree
- Lowest Common Ancestor
- Path Sum
- Left View
- Right View
- Zigzag traversal
Advanced
- Maximum path sum
- Serialize and deserialize
- Construct tree from traversals
- Boundary traversal
- Vertical traversal
- Top view
- Bottom view
- Flatten binary tree
- Nodes at distance K
- Morris traversal
4.9.49Quick Revision
Remember the basic structure:
Node
/ \
Left Right
- Each node has at most two children.
- The four major traversals:
- Preorder:
- Node → Left → Right
- Inorder:
- Left → Node → Right
- Postorder:
- Left → Right → Node
- Level Order:
- Level by Level
- For a tree with n nodes:
- Traversal → O(n)
Search → O(n)
Height → O(n)
Recursion uses:
O(h)
- auxiliary space.
- The key structural types are:
- Binary Tree
↓
┌───┼─────────────┐
↓ ↓ ↓
Full Complete Perfect
And the most important distinction is:
A Binary Tree only restricts each node to at most two children. A Binary Search Tree additionally imposes an ordering relationship between left and right subtrees.
That distinction leads directly into 4.10 Binary Search Trees, where the tree structure is combined with an ordering property to support much faster searching in balanced cases.
4.10Binary Search Trees
4.10Binary Search Trees
A Binary Search Tree (BST) is a specialized binary tree that maintains an ordering relationship between the values in its left and right subtrees.
For every node:
Left Subtree < Node < Right Subtree
Example:
50
/ \
30 70
/ \ / \
20 40 60 80
Here:
- All values in the left subtree of 50 are smaller than 50.
- All values in the right subtree of 50 are greater than 50.
- The same rule applies recursively to every node.
This ordering makes searching much more efficient than searching an ordinary binary tree when the BST is reasonably balanced.
4.10.1Binary Tree vs Binary Search Tree
- A Binary Tree only requires:
- Each node has at most 2 children
- It does not require any ordering.
Example:
50
/ \
80 20
This is a valid binary tree.
But it is not a valid BST because:
80 > 50
while 80 is in the left subtree.
A BST requires:
50
/ \
< 50 > 50
4.10.2BST Property
Consider:
50
/ \
30 70
/ \ / \
20 40 60 80
For node 50:
Left subtree → 20, 30, 40
Right subtree → 60, 70, 80
Therefore:
20 < 30 < 40 < 50 < 60 < 70 < 80
The ordering is recursively maintained throughout the tree.
4.10.3BST Node
A BST node can be represented using the same structure as a binary-tree node:
class TreeNode:
def __init__(self, value):
self.value = value- self.left = None
- self.right = None
- Create the root:
- root = TreeNode(50)
4.10.4Searching in a BST
- Searching is the most important advantage of a BST.
- Suppose we have:
- 50
/ \
30 70
/ \ / \
20 40 60 80
- We want to find:
- 60
- Start at 50.
- Since:
- 60 > 50
- we go right.
- 50
\
70
/
so go left.
We find 60.
Instead of examining every node, we eliminate an entire subtree at every step.
4.10.5BST Search Algorithm
def search(root, target):
if root is None:
return False
if root.value == target:
return True
if target < root.value:
return search(root.left, target)
return search(root.right, target)
Example:
print(search(root, 60))
Output:
True
4.10.6Iterative BST Search
The same operation can be implemented without recursion:
def search(root, target):
current = root
while current:
if current.value == target:
return True
if target < current.value:
current = current.left
else:
current = current.right
return False
This avoids recursion stack usage.
4.10.7BST Search Complexity
- The complexity depends on the height of the tree.
- Time = O(h)
- where h is the height.
- For a balanced BST:
- h = O(log n)
Therefore:
Search = O(log n)
For a completely skewed BST:
h = O(n)
Therefore:
Search = O(n)
This distinction is extremely important.
4.10.8Inserting into a BST
- To insert a value, we follow the BST ordering rule.
- Consider:
- 50
/ \
30 70
- Insert:
- 40
- Since:
- 40 < 50
- go left.
- Then:
- 40 > 30
- go right.
- So:
- 50
/ \
30 70
\
40
4.10.9BST Insert Algorithm
def insert(root, value):
if root is None:
return TreeNode(value)
if value < root.value:
root.left = insert(root.left, value)elif value > root.value:
root.right = insert(root.right, value)return root
Usage:
root = None
for value in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, value)
Result:
50
/ \
30 70
/ \ / \
20 40 60 80
4.10.10BST Insertion Complexity
Insertion follows one path from root to the insertion location.
Therefore:
- Time = O(h)
- Balanced:
- O(log n)
- Skewed:
O(n)
4.10.11Handling Duplicate Values
- A BST needs a rule for duplicate values.
- Common strategies include:
- Strategy 1 — Ignore duplicates
if value < root.value:
...
elif value > root.value:
...
- Equal values are ignored.
- Strategy 2 — Store duplicates on the left
- value <= node
- Strategy 3 — Store duplicates on the right
- value >= node
- Strategy 4 — Store a count
- Instead of creating multiple nodes:
- Node:
value = 50
count = 3
The appropriate strategy depends on the application.
4.10.12Inorder Traversal of a BST
One of the most important BST properties is:
- Inorder traversal produces values in sorted order.
- Consider:
- 50
/ \
30 70
/ \ / \
20 40 60 80
- Inorder:
- 20 30 40 50 60 70 80
- Recall:
- Inorder:
- Left → Node → Right
This is why inorder traversal is extremely important for BST problems.
4.10.13Getting Sorted Values from a BST
def inorder(root, result):
if root is None:
return- inorder(root.left, result)
- result.append(root.value)
- inorder(root.right, result)
- Usage:
result = []
inorder(root, result)
print(result)
Output:
\[20, 30, 40, 50, 60, 70, 80\]
A BST can therefore act as an ordered data structure.
4.10.14Finding Minimum Value
In a BST, the minimum value is always located at the leftmost node.
Example:
50
/
30
/
20
/
- 10
- Minimum:
- 10
- Algorithm:
def find_min(root):
if root is None:
return None
current = root
while current.left:
current = current.left
return current.value
Complexity:
O(h)
4.10.15Finding Maximum Value
Similarly, the maximum value is at the rightmost node.
def find_max(root):
if root is None:
return None
current = root
while current.right:
current = current.right
return current.value
Complexity:
O(h)
Balanced BST:
O(log n)
4.10.16BST Deletion
- Deletion is the most complicated basic BST operation.
- There are three major cases:
- Delete a leaf node.
- Delete a node with one child.
- Delete a node with two children.
4.10.17Case 1 — Delete a Leaf Node
Consider:
50
/ \
30 70
/
Since 20 has no children, simply remove it:
50
/ \
30 70
This is the simplest deletion case.
4.10.18Case 2 — Delete a Node with One Child
Consider:
50
/
30
/
- 20
- Delete:
- 30
- Node 30 has one child:
- 20
- Replace 30 with 20:
- 50
/
20
4.10.19Case 3 — Delete a Node with Two Children
Consider:
50
/ \
30 70
/ \ / \
20 40 60 80
- Delete:
- 50
- The node has two children.
A common strategy is to replace it with its:
- Inorder successor
- The inorder successor is the smallest value in the right subtree.
- Right subtree:
- 70
/ \
60 80
- Smallest value:
- 60
- Replace 50 with 60:
- 60
/ \
30 70
/ \ \
20 40 80
Then remove the original 60.
4.10.20Inorder Successor
- The inorder successor of a node is the next larger value in sorted order.
- For:
- 20 30 40 50 60 70 80
- the successor of 50 is:
- 60
If the node has a right subtree, the successor is usually the leftmost node in that right subtree.
4.10.21Inorder Predecessor
- The inorder predecessor is the next smaller value.
- For:
- 20 30 40 50 60 70 80
- the predecessor of 50 is:
- 40
If the node has a left subtree, the predecessor is usually the rightmost node in that left subtree.
4.10.22BST Delete Implementation
def delete(root, value):
if root is None:
return None
if value < root.value:
root.left = delete(root.left, value)elif value > root.value:
root.right = delete(root.right, value)else:
# Case 1 and Case 2if root.left is None:
return root.right
if root.right is None:
return root.left
# Case 3: two children
successor = root.right
while successor.left:
successor = successor.left
- root.value = successor.value
- root.right = delete(
- root.right,
- successor.value
)
return root
Complexity:
O(h)
4.10.23BST Complexity
For a BST with height h:
| Operation | Complexity |
|---|
| Search | O(h) |
| Insert | O(h) |
| Delete | O(h) |
| Find Minimum | O(h) |
| Find Maximum | O(h) |
| Inorder Traversal | O(n) |
Balanced BST:
h = O(log n)
Therefore:
- Search → O(log n)
- Insert → O(log n)
- Delete → O(log n)
- Worst-case skewed BST:
h = O(n)
Therefore:
- Search → O(n)
- Insert → O(n)
- Delete → O(n)
4.10.24Best, Average and Worst Cases
BST performance depends heavily on tree shape.
Best Case
A very balanced structure:
50
/ \
30 70
Height is small.
Average Case
For reasonably distributed insertions, performance can often be around:
O(log n)
but the exact behavior depends on the insertion sequence and assumptions.
Worst Case
- Insert sorted values:
- 10
- 20
- 30
- 40
- 50
- The tree becomes:
- 10
\
20
\
30
\
40
\
50
Now:
h = O(n)
and operations degrade to:
O(n)
4.10.25Why Sorted Insertion Is Dangerous
- Suppose we insert:
- 10, 20, 30, 40, 50, 60
- The BST becomes:
- 10
\
20
\
30
\
40
\
50
\
60
It has effectively become a linked list.
Therefore, a basic BST does not automatically guarantee O(log n) performance.
4.10.26Balanced Binary Search Trees
- To prevent severe skewing, specialized self-balancing BSTs are used.
- Important examples:
- AVL Tree
- Red-Black Tree
- They maintain height close to:
- O(log n)
Therefore, operations remain approximately:
- Search → O(log n)
- Insert → O(log n)
- Delete → O(log n)
These structures are especially important in systems that need reliable ordered-map/set performance.
4.10.27AVL Tree
- An AVL Tree is a self-balancing Binary Search Tree.
- For every node:
- |height(left) - height(right)| ≤ 1
If an insertion or deletion makes the tree unbalanced, rotations are performed.
Example:
10
\
20
\
30
This is unbalanced.
An AVL rotation can transform it into:
20
/ \
10 30
The tree becomes balanced.
4.10.28Red-Black Tree
- A Red-Black Tree is another self-balancing BST.
- Each node has an additional color:
- Red
- Black
The tree follows specific coloring rules that ensure its height remains O(log n).
Red-Black Trees are widely used in implementations of ordered maps and sets in various programming languages and libraries.
4.10.29BST Rotations
- Rotations are local restructuring operations used to maintain balance.
- Right Rotation
- Before:
- 30
/
20
/
- 10
- After right rotation:
- 20
/ \
10 30
\
20
\
/ \
10 30
Rotations preserve the BST ordering property.
4.10.30Kth Smallest Element
Because inorder traversal of a BST is sorted, the kth smallest element can be found using inorder traversal.
Example:
- 20 30 40 50 60 70 80
- The:
- 1st smallest = 20
- 2nd smallest = 30
- 3rd smallest = 40
- Implementation:
def kth_smallest(root, k):
stack = []
current = root
while True:
while current:
stack.append(current)current = current.left
current = stack.pop()
k -= 1
if k == 0:
return current.value
current = current.right
This is a very common interview problem.
4.10.31Kth Largest Element
Similarly, reverse inorder traversal gives values from largest to smallest:
Right → Node → Left
Example:
80 70 60 50 40 30 20
The kth largest can therefore be found efficiently using reverse inorder traversal.
4.10.32Validate a BST
- A common interview problem asks:
- Is this binary tree actually a valid BST?
- Consider:
- 10
/ \
5 15
/ \
6 20
This is not a valid BST.
Why?
Because 6 is in the right subtree of 10, but:
6 < 10
Simply comparing each node with its immediate parent is not sufficient.
4.10.33Valid BST Using Bounds
A robust approach passes valid minimum and maximum bounds.
def is_valid_bst(root, low=float("-inf"), high=float("inf")):
if root is None:
return True
if not (low < root.value < high):
return False
return (
is_valid_bst(root.left, low, root.value)
and is_valid_bst(root.right, root.value, high)
)Complexity:
Time = O(n)
Space = O(h)
This is an important recursive pattern.
4.10.34BST as a Sorted Set
- A BST can be used conceptually to maintain a collection of unique values while preserving sorted order.
- For example:
- Insert:
- 50, 20, 70, 10, 30
- Inorder traversal produces:
- 10 20 30 50 70
This makes BSTs useful when we need:
- Ordered data
- Search
- Insert
- Delete
- Minimum/maximum
- Predecessor/successor
4.10.35Predecessor and Successor
For:
50
/ \
30 70
/ \ / \
20 40 60 80
For 50:
Predecessor = 40
Successor = 60
These operations are important in ordered data structures.
4.10.36Range Search
- Suppose we want all values between:
- 35 and 65
- in:
- 50
/ \
30 70
/ \ / \
20 40 60 80
The values are:
40, 50, 60
A BST lets us skip subtrees that cannot contain values in the desired range.
This can be much more efficient than scanning every node, depending on tree balance and the size of the output.
4.10.37BST and Recursion
BST algorithms naturally follow the recursive structure:
Node
/ \
Smaller Larger
For searching:
target < node
▼
left subtree
or:
target > node
▼
right subtree
This is why recursion is particularly natural for BSTs.
4.10.38BST and Inorder Traversal
This relationship is worth memorizing:
BST
▼
Inorder Traversal
▼
Sorted Order
If:
BST = 50, 30, 70, 20, 40, 60, 80
then:
Inorder = 20, 30, 40, 50, 60, 70, 80
- This property is used in:
- Kth smallest
- Kth largest
- BST validation
- Sorted output
- Predecessor/successor
- Range queries
4.10.39Building a Balanced BST from a Sorted Array
Suppose we have:
\[1, 2, 3, 4, 5, 6, 7\]
Choosing the middle element as the root:
4
/ \
2 6
/ \ / \
1 3 5 7
produces a balanced BST.
Implementation:
def sorted_array_to_bst(numbers):
if not numbers:
return None
middle = len(numbers) // 2
root = TreeNode(numbers[middle])
root.left = sorted_array_to_bst(
numbers[:middle]
)
root.right = sorted_array_to_bst(
numbers[middle + 1:]
)
return root
Complexity with slicing:
can involve extra copying. An index-based implementation avoids that extra copying and achieves:
Time = O(n)
Space = O(log n)
for a balanced result, excluding the output tree itself.
4.10.40BST Applications
- BSTs are useful when applications require ordered data with dynamic updates.
- Examples include:
- Ordered sets
- Ordered maps
- Symbol tables
- Searching
- Range queries
- Scheduling
- Maintaining sorted data
- Predecessor/successor queries
- Database/indexing concepts
Self-balancing variants are particularly useful when predictable logarithmic performance is required.
4.10.41BST in Database Systems
Database indexes often use tree-based structures, although practical database systems commonly use B-Trees or B+ Trees rather than ordinary binary search trees.
The fundamental idea is similar:
Key
▼
Ordered structure
▼
Efficient search
For example:
Find customer ID = 105023
An index can avoid scanning every record.
This is one reason tree-based indexing is fundamental in database systems.
4.10.42BST vs Hash Table
This is an important interview comparison.
| Feature | BST | Hash Table |
|---|
| Search | O(h) | O(1) average |
| Insert | O(h) | O(1) average |
| Delete | O(h) | O(1) average |
| Sorted order | Yes | Not the primary purpose |
| Min/Max | O(h) | Not naturally ordered |
| Range queries | Good with ordered tree | Usually less natural |
| Worst-case basic structure | O(n) | O(n) |
| Balanced BST | O(log n) | O(1) average |
- Use a hash table when:
- Fast exact key lookup is the main requirement.
- Use a BST/ordered tree when:
- Ordering, ranges, predecessor/successor, or sorted traversal matter.
4.10.43BST vs Heap
| Feature | BST | Heap |
|---|
| Main purpose | Ordered searching | Priority access |
| Search arbitrary value | Efficient when balanced | O(n) |
| Minimum | O(log n) in balanced BST | O(1) in min-heap |
| Maximum | O(log n) in balanced BST | O(n) in min-heap |
| Insert | O(log n) balanced | O(log n) |
| Delete arbitrary | O(log n) balanced | Usually O(n) without extra indexing |
| Sorted traversal | Yes | No |
| Structure | Ordered by subtrees | Complete binary tree |
A heap is primarily designed for priority queues.
A BST is designed for ordered searching.
4.10.44Common BST Interview Problems
Beginner
- Search in a BST.
- Insert into a BST.
- Find minimum.
- Find maximum.
- Inorder traversal.
- Find predecessor.
- Find successor.
- Calculate height.
Intermediate
- Delete a node.
- Validate BST.
- Kth smallest element.
- Kth largest element.
- Lowest Common Ancestor in BST.
- Range Sum of BST.
- Search for a value range.
- Convert sorted array to BST.
- Find closest value.
- Find floor and ceiling.
Advanced
- Construct BST from preorder.
- Recover a corrupted BST.
- Serialize and deserialize BST.
- Balance an unbalanced BST.
- Trim a BST.
- Merge two BSTs.
- Two Sum in a BST.
- Find median in a BST.
Convert BST to sorted doubly linked list.
Implement an iterator for BST.
4.10.45Important BST Interview Pattern: Two Sum
Given a BST:
5
/ \
3 6
/ \ \
2 4 7
- Target:
- 9
- Possible pair:
- 2 + 7 = 9
- One approach is:
BST
▼
Inorder
▼
Sorted Array
▼
Two Pointers
This combines BST traversal with the Two Pointers pattern.
4.10.46Important BST Interview Pattern: Floor
The floor of a target is the greatest value in the BST that is less than or equal to the target.
- Suppose:
- Values:
- 10, 20, 30, 40, 50
- For:
target = 35
target = 40
the floor is:
40
BST ordering allows us to find the answer efficiently.
4.10.47Important BST Interview Pattern: Ceiling
- The ceiling is the smallest value greater than or equal to the target.
- For:
- Values:
- 10, 20, 30, 40, 50
- and:
target = 35
the ceiling is:
40
Floor and ceiling problems are common in ordered-set implementations.
4.10.48BST Iterator
- A BST iterator can return values in ascending order.
- For:
- 50
/ \
30 70
/ \ / \
20 40 60 80
- the iterator should produce:
- 20
- 30
- 40
- 50
- 60
- 70
- 80
- A stack can simulate inorder traversal without storing the entire sorted list.
- This combines:
- BST
+
Inorder Traversal
+
Stack
and is a common interview problem.
4.10.49Complexity Summary
For a BST with height h:
| Operation | Complexity |
|---|
| Search | O(h) |
| Insert | O(h) |
| Delete | O(h) |
| Min | O(h) |
| Max | O(h) |
| Predecessor | O(h) |
| Successor | O(h) |
| Inorder traversal | O(n) |
Balanced BST
h = O(log n)
Therefore:
Search → O(log n)
Insert → O(log n)
Delete → O(log n)
Skewed BST
h = O(n)
Therefore:
Search → O(n)
Insert → O(n)
Delete → O(n)
4.10.50Key Takeaways
The most important BST rule is:
Node
/ \
< >
/ \
Smaller Larger
For every node:
All values in the left subtree are smaller, and all values in the right subtree are larger, subject to the chosen duplicate policy.
- Remember:
- Search → O(h)
- Insert → O(h)
- Delete → O(h)
- where h is the tree height.
- For a balanced BST:
- h = O(log n)
- so:
- Search → O(log n)
- Insert → O(log n)
- Delete → O(log n)
The most important BST property to memorize is:
BST
▼
Inorder Traversal
▼
Sorted Order
And the three deletion cases are:
1. Leaf node
↓
Remove
2. One child
↓
Replace with child
3. Two children
↓
Replace with inorder
successor/predecessor
Finally, remember the major distinction:
A normal Binary Search Tree can degrade to O(n). Self-balancing BSTs such as AVL and Red-Black Trees maintain O(log n) height.
The next topic, 4.11 Heaps, changes the focus from ordered searching to efficient priority-based access.
Heaps
A Heap is a special complete binary tree used when we need to repeatedly get the minimum or maximum element quickly.
- The two main types are:
- Min Heap → smallest element is always at the top.
- Max Heap → largest element is always at the top.
1. What is a Heap?
Consider this Min Heap:
10
/ \
20 15
/ \ / \
30 40 25 50
- Notice:
- Parent <= Children
- So:
- 10 ≤ 20
- 10 ≤ 15
- 20 ≤ 30
- 20 ≤ 40
- 15 ≤ 25
- 15 ≤ 50
Therefore, this is a Min Heap.
For a Max Heap, the opposite is true:
50
/ \
40 45
/ \ / \
20 30 35 10
Here:
Parent >= Children
2. Two Important Properties
A heap has two properties.
- Property 1: Complete Binary Tree
- All levels are completely filled except possibly the last level.
- The last level is filled from left to right.
- Valid:
- 10
/ \
20 30
/ \ /
/ \
20 30
\ \
40 50
- Property 2: Heap Order
- For a Min Heap:
- Parent <= Child
- For a Max Heap:
- Parent >= Child
3. Why do we need Heaps?
Suppose you have:
\[50, 20, 10, 40, 30, 60\]
- And you repeatedly need the smallest number.
- Sorting first would cost:
- O(n log n)
- A heap allows us to efficiently maintain the smallest element.
- With a Min Heap:
- 10
/ \
20 60
/ \ /
40 30 50
The minimum is always:
root = 10
Getting it is:
O(1)
4. Heap vs Binary Search Tree
This is an important interview question.
BST
50
/ \
30 70
/ \ / \
20 40 60 80
- BST property:
- Left < Root < Right
- Heap
- 20
/ \
30 40
/ \ / \
50 60 70 80
Heap property:
Parent <= Children
A heap does not guarantee that the left subtree is smaller than the right subtree.
5. Heap Representation Using an Array
This is one of the most important concepts.
- We don't normally need to create a tree using nodes.
- Instead:
- 10
/ \
20 15
/ \ /
30 40 25
can be stored as:
\[10, 20, 15, 30, 40, 25\]
- The relationship between parent and children can be calculated using indexes.
- For an element at index i:
- Parent
- parent = (i - 1) // 2
- Left child
left = 2 * i + 1
Right child
right = 2 * i + 2
Example:
Array:
index: 0 1 2 3 4 5
\[10, 20, 15, 30, 40, 25\]
For index 1:
value = 20
left child = 2(1) + 1 = 3
right child = 2(1) + 2 = 4
Therefore:
20
/ \
30 40
6. Inserting into a Heap
Suppose we have:
10
/ \
20 15
/ \
30 40
Array:
\[10, 20, 15, 30, 40\]
Now insert:
5
First, put it at the next available position:
10
/ \
20 15
/ \ /
30 40 5
But this violates the Min Heap property:
5 < 15
So we move 5 upward.
- This operation is called:
- Heapify Up
- 10
/ \
20 5
/ \ /
30 40 15
- Still:
- 5 < 10
- Move again:
- 5
/ \
20 10
/ \ /
30 40 15
Now the heap is valid.
- Complexity
- Insertion:
- O(log n)
because the element can move from the bottom to the root.
7. Removing the Minimum
In a Min Heap, the minimum is always:
/ \
20 10
/ \ /
30 40 15
We want to remove 5.
We replace the root with the last element:
15
/ \
20 10
/ \
30 40
Now the heap property is broken:
15 > 10
So we move 15 downward.
This is called:
Heapify Down
10
/ \
20 15
/ \
- 30 40
- Complexity
- Extract Min = O(log n)
8. Heap Operations
| Operation | Complexity |
|---|
| Get Min/Max | O(1) |
| Insert | O(log n) |
| Extract Min/Max | O(log n) |
| Delete | O(log n) |
| Search | O(n) |
| Build Heap | O(n) |
That last one is important:
Building a heap from an unsorted array can be done in O(n), not O(n log n).
9. Python Heap
Python provides a built-in module:
import heapq
Python's heapq implements a Min Heap.
Create heap
import heapq
nums = [40, 10, 30, 20, 50]
heapq.heapify(nums)
print(nums)
The exact array arrangement may look like:
\[10, 20, 30, 40, 50\]
The important point is that it satisfies the heap property.
10. Insert
Use:
heapq.heappush(heap, value)
Example:
import heapq
heap = []
- heapq.heappush(heap, 30)
- heapq.heappush(heap, 10)
- heapq.heappush(heap, 20)
print(heap)
Conceptually:
10
/ \
30 20
11. Get Minimum
Simply:
heap[0]
Example:
print(heap[0])
Complexity:
O(1)
12. Remove Minimum
Use:
heapq.heappop(heap)
Example:
minimum = heapq.heappop(heap)
print(minimum)
- This removes and returns the smallest element.
- Complexity:
- O(log n)
13. Max Heap in Python
Python's heapq is a Min Heap.
To simulate a Max Heap, store negative values.
import heapq
heap = []
- heapq.heappush(heap, -30)
- heapq.heappush(heap, -10)
- heapq.heappush(heap, -20)
maximum = -heapq.heappop(heap)
print(maximum)
Output:
\[-30, -10, -20\]
The smallest negative number corresponds to the largest original number.
14. Very Important: Heap ≠ Sorted Array
Suppose:
heap = [10, 20, 15, 40, 30, 25]
You cannot assume:
- 10 < 20 < 15 < 40 < 30 < 25
- The heap only guarantees:
- parent <= children
- So:
- 10
/ \
20 15
/ \ /
40 30 25
The array is not necessarily sorted.
15. Where are Heaps Used?
Heaps are extremely important in coding interviews.
Common applications:
1. Priority Queue
Highest priority item comes first
2. Top K Problems
- Examples:
- Top K largest numbers
- Top K smallest numbers
- Kth largest element
- Kth smallest element
3. Scheduling
- CPU scheduling
- Task scheduling
- Event processing
4. Dijkstra's Algorithm
Priority queues/heaps are commonly used to efficiently select the next closest vertex.
5. Merge K Sorted Lists
A heap can keep track of the smallest current element from each list.
6. Median of a Stream
Use:
Max Heap + Min Heap
This is a very common interview problem.
16. The Most Important Heap Pattern
- If the problem says:
- "Find the K largest/smallest..."
- Immediately think:
- HEAP
- For example:
- Find the 3rd largest number.
- Instead of sorting the entire array:
- sorted(nums)
we can maintain a heap of size k.
For a large dataset:
n = 1,000,000
k = 10
A heap of size 10 can be much more efficient than fully sorting one million elements.
17. Heap Mental Model
Remember this:
HEAP
│
┌────────┴────────┐
│ │
Min Heap Max Heap
│ │
minimum top maximum top
│ │
Parent ≤ Parent ≥
children children
And the core operations:
DELETE ROOT
▼
Replace with last
▼
Heapify Down
🎯 What you should master for interviews
For Heaps, make sure you can explain and implement these without looking at notes:
- Min Heap
- Max Heap
- Complete Binary Tree
- Array representation
- Parent/child index formulas
- Heapify Up
- Heapify Down
- Insert
- Extract Min/Max
- Build Heap
- Priority Queue
- Kth Largest / Kth Smallest
- Top K Frequent Elements
- Merge K Sorted Lists
- Find Median from Data Stream
The next natural step after understanding this is to implement a Min Heap from scratch in Python, without using heapq. That will make the heap concept much clearer.