Skip to content

Intermediate: core algorithms · Trees and binary search trees · topic 35 of 61 · 40 min

Lowest common ancestor, serialisation and balanced trees

Explanation

The lowest common ancestor (LCA) of two nodes is the deepest node that has both of them below it (a node counts as below itself). In a BST you find it by walking down until the two values split; in any binary tree a postorder search finds it in O(n). Serialising turns a tree into a string (preorder with markers for empty children) so it can be saved or sent and rebuilt. Self-balancing trees such as AVL and red-black trees keep the height O(log n); B-trees do the same job for databases on disk.

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.

python
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 4
2 is an ancestor of 4, so the LCA of 2 and 4 is 2 itself.

LCA 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.

python
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 3
This version assumes both values are in the tree and all values are different.

Serialise 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.

python
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: True
Both are O(n). An iterator hands out the tokens one at a time in the same preorder they were written.

Level 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.

output
    y                x
   / \             /   \
  x   C    -->    A     y
 / \                   / \
A   B                 B   C
A right rotation at y. The inorder order A, x, B, y, C stays the same; x moves up and y moves down.
  • 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

LCA, serialisation and balanced trees

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.

python
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"])
Shows True Mobiles: the tree survives the round trip through a string.

Practice (0/4 done)

Task 1 · Code · Python

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.

Task 2 · Code · Python

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.

Task 3 · Code · Python

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.

Task 4 · Code · Javascript

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

Question 1

1.In a BST, p = 3 and q = 9, and the current node is 6. What is the LCA?

Question 2

2.Why does serialising a tree with preorder need a marker for empty children?

Question 3

3.Which data structure is behind Java's TreeMap?

Question 4

4.Why do databases use B-trees for indexes instead of binary search trees?

Question 5

5.What does a rotation in an AVL or red-black tree keep the same?

Read more (free)

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

Answer: In its left subtree

That is the BST property.

From Binary search trees

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).

From Recursive tree patterns

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.

From Trees: words and shapes

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

  1. This is the last topic of “Trees and binary search trees”. The module exam comes next.
  2. Module 7 exam: Trees and binary search trees · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Heaps and greedy algorithms. 61 topics left in the course before the final exam.

← Previous: Binary search trees