Intermediate: core algorithms · Trees and binary search trees · topic 35 of 61 · 40 min
Lowest common ancestor, serialisation and balanced trees
Explanation
Lowest common ancestor
An ancestor of a node is any node on the path from the root down to it, including the node itself. The lowest common ancestor of p and q is the deepest node that is an ancestor of both. In a family tree it is the closest shared grandparent; in a folder tree it is the deepest folder that contains both files; in an org chart it is the lowest manager both people report up to.
LCA in a BST: follow the split
In a BST the values tell you where to go. If both p and q are smaller than the current node, the LCA is in the left subtree. If both are larger, it is in the right subtree. Otherwise they split here (one on each side, or one of them IS this node), so the current node is the LCA. This walks one path: O(h) time and O(1) space with a loop.
def lca_bst(root, p, q):
node = root
while node:
if p < node.val and q < node.val:
node = node.left
elif p > node.val and q > node.val:
node = node.right
else:
return node.val
return None
bst = build([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5])
print(lca_bst(bst, 2, 8), lca_bst(bst, 2, 4), lca_bst(bst, 3, 5)) # Output: 6 2 4LCA in any binary tree
Without the BST order you must search. Ask each subtree: "did you find p or q?" A subtree returns the matching node if it found one, or None. If the current node is p or q, return it at once. If BOTH the left and right calls return something, p and q are on different sides, so the current node is the LCA. If only one side returns something, pass that up. Every node is visited at most once: O(n) time, O(h) space.
def lca(node, p, q):
if node is None or node.val == p or node.val == q:
return node
left = lca(node.left, p, q)
right = lca(node.right, p, q)
if left and right:
return node # p and q are on different sides
return left or right
t = build([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4])
print(lca(t, 5, 1).val, lca(t, 5, 4).val, lca(t, 7, 8).val) # Output: 3 5 3Serialise and deserialise
Serialising means turning a structure into a string (or bytes) so it can be saved to a file, stored in a database or sent over a network. Deserialising rebuilds the same structure from the string. A plain preorder list is not enough: [1, 2] could mean "2 is the left child" or "2 is the right child". Adding a marker such as "#" for every missing child removes the doubt, and then preorder alone describes the tree exactly.
def serialize(root):
parts = []
def walk(node):
if node is None:
parts.append("#")
return
parts.append(str(node.val))
walk(node.left)
walk(node.right)
walk(root)
return ",".join(parts)
def deserialize(data):
tokens = iter(data.split(","))
def make():
tok = next(tokens)
if tok == "#":
return None
node = TreeNode(int(tok))
node.left = make() # the same order as serialize: node, left, right
node.right = make()
return node
return make()
s = serialize(build([1, 2, 3, None, None, 4, 5]))
print(s) # Output: 1,2,#,#,3,4,#,#,5,#,#
print(serialize(deserialize(s)) == s) # Output: TrueLevel order with markers works too (that is how LeetCode shows trees and how build() reads them). JSON with nested objects is another common choice in APIs.
Keeping trees balanced
A plain BST can become a chain. Self-balancing BSTs reshape themselves after every insert and delete so the height stays O(log n), which makes search, insert and delete O(log n) in the worst case. They use rotations: a small local change of links that keeps the BST order but moves one node up and another down.
y x
/ \ / \
x C --> A y
/ \ / \
A B B C- AVL tree: keeps the strict rule that the two subtree heights of every node differ by at most 1. After an insert or delete it walks back up and fixes any broken node with one or two rotations. Very well balanced, so lookups are fast.
- Red-black tree: colours every node red or black and follows a few colour rules, which guarantee that the longest root-to-leaf path is at most twice the shortest. Slightly less balanced than AVL, but usually fewer rotations on updates. Java's TreeMap and TreeSet are red-black trees, and C++ std::map and std::set are usually implemented as red-black trees.
In interviews you are rarely asked to code AVL or red-black trees from memory. Know what they guarantee (O(log n) height), the idea of a rotation, and where they are used. Python has no balanced tree in its standard library; people use dict and set for lookups, a sorted list with bisect for ordered data, or a third-party package for a sorted container.
B-trees: balanced trees for disks
Reading from a disk or SSD is slow compared with memory, and it reads a whole block (page) at a time. A B-tree stores many keys in each node, often hundreds, so each node fills a page and the tree is very wide and very shallow. A few levels can index millions of rows, so a lookup needs only a few page reads. That is why most relational databases use B-trees (or the B+ tree variant) for their indexes: PostgreSQL's default index type is B-tree, and MySQL's InnoDB stores tables and indexes as B+ trees. File systems use them too.
Common mistakes
- In the BST LCA, forgetting the case where p or q equals the current node. Fix: use "otherwise return node" after the two "both smaller" and "both larger" checks.
- Using the BST shortcut on a general binary tree. Fix: only follow values left or right when the tree is promised to be a BST.
- Serialising without null markers, or deserialising in a different order than you serialised. Fix: write and read in the same preorder, with a marker for every None.
- Splitting on a comma but storing values that contain commas (for example names). Fix: pick a safe separator, escape it, or use JSON.
- Saying a BST lookup is always O(log n). Fix: say O(h); it is O(log n) only for a balanced tree such as AVL or red-black.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Lowest common ancestor, serialisation and balanced trees · 1/8
This lesson covers the lowest common ancestor, saving a tree as a string, and the balanced trees used inside libraries and databases.
Prefer a video by a teacher? Find free videos on “lowest common ancestor binary tree serialize deserialize avl red black tree explained” on YouTube ↗
In real life: Saving a category tree and indexing a database
A shopping app downloads its category tree from a server when it starts. The server serialises the tree into text (usually JSON) and the app rebuilds it, exactly the serialise and deserialise round trip. When the app then searches orders by order number, the database usually answers through a B-tree index: a wide, shallow balanced tree where each node holds many keys, so only a few disk pages are read per lookup.
import json
menu = {"name": "All", "children": [
{"name": "Electronics", "children": [{"name": "Mobiles", "children": []}]},
{"name": "Fashion", "children": []},
]}
text = json.dumps(menu) # serialise
again = json.loads(text) # deserialise
print(again == menu, again["children"][0]["children"][0]["name"])Practice (0/4 done)
Write lca_bst(root, p, q) for a BST with distinct values. p and q are VALUES that are both in the tree (they may be equal). Return the VALUE of their lowest common ancestor. A node is an ancestor of itself. Use the BST order: O(h) time. Example: for build([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]), lca_bst(root, 2, 4) returns 2.
Write lca_binary_tree(root, p, q) for a binary tree that is NOT ordered (not a BST). All values are distinct, and p and q are values that are both in the tree. Return the VALUE of their lowest common ancestor (a node is an ancestor of itself). Example: for build([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4]), lca_binary_tree(root, 5, 4) returns 5.
Write serialize(root) that turns a binary tree into a STRING, and deserialize(data) that rebuilds exactly the same tree (same values, same shape) from that string. Values are integers, possibly negative or repeated. Any format works as long as the round trip is exact; preorder with "#" for empty children is a good choice. An empty tree must also round-trip.
In JavaScript, a tree is given as nested objects { val, left, right }, where a missing child is null (or the key is missing). Write maxDepth(node) that returns the number of nodes on the longest path from the root down to a leaf; an empty tree (null) returns 0. Example: maxDepth({ val: 1, left: { val: 2, left: null, right: null }, right: null }) returns 2.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Traversals: DFS and BFSTrees and binary search trees · same module
- Binary search treesC Programming (from zero to systems) · Data structures in C
- How indexes workSQL and Databases · Indexes, query plans and views
- Tries (prefix trees)Tries, union-find and bit manipulation
- Data-structure questions in JavaScriptJavaScript Programming (language in depth) · Interview preparation and career
- Big-O and searchingPython Programming · Algorithms, data structures and testing
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
In a BST, p = 3 and q = 9, and the current node is 6. What is the LCA?
Answer: 6
One value is smaller and one is larger, so they split here: 6 is the lowest node with both below it.
Why does serialising a tree with preorder need a marker for empty children?
Answer: Without markers different shapes, like 2 as a left child or as a right child, give the same list
The markers record where each subtree ends, so the shape can be rebuilt exactly.
Which data structure is behind Java's TreeMap?
Answer: A red-black tree
The Java documentation describes TreeMap as a red-black tree based map, so its operations are O(log n).
Why do databases use B-trees for indexes instead of binary search trees?
Answer: Each B-tree node holds many keys, so the tree is shallow and a lookup reads only a few disk pages
Disk reads come in pages; a wide node fills a page, so a few levels cover millions of rows.
What does a rotation in an AVL or red-black tree keep the same?
Answer: The inorder (sorted) order of the values
A rotation only rearranges a few links. The BST order is kept while the height changes.
You insert 10, 20, 30, 40, 50 in that order into an empty BST. What is its height in edges?Related
Answer: 4
Each value is larger than all before it, so it goes right every time: a chain of 5 nodes with 4 edges.
From Binary search trees
In a BST, where are keys smaller than a node's key?Related
What is the extra space of a recursive tree function on a balanced tree of n nodes?Related
Answer: O(log n)
The recursion stack holds one path from the root, and a balanced tree has height O(log n).
Why is searching down one path O(log n) in a balanced tree but O(n) in a skewed one?Related
Answer: The cost is O(height), and the height is about log n when balanced but n − 1 when skewed
Walking down one path costs one step per level, and the number of levels is the height.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Trees and binary search trees”. The module exam comes next.
- Module 7 exam: Trees and binary search trees · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Heaps and greedy algorithms. 61 topics left in the course before the final exam.