datarekha

Balanced Trees & B-Trees

Why a plain BST can collapse to O(n), how self-balancing trees fix it with rotations, and why databases reach for B+-trees instead of binary trees for their indexes.

7 min read Advanced Data Structures & Algorithms Lesson 19 of 32

What you'll learn

  • Why a BST built from sorted input collapses to a linked list, and what 'balance' really means
  • How a rotation relinks a few pointers to lower height without breaking the BST rule
  • Why B-trees use wide fan-out instead of binary splits, mapping each node to a disk page
  • Why almost every SQL index is a B+-tree, costing 3-4 disk reads instead of n

Before you start

A binary search tree promises O(log n) search — but that promise holds only while the tree stays balanced. Feed it the wrong order of data and the promise quietly evaporates.

The collapse

Insert 1, 2, 3, 4, 5 in that order. Each value is larger than everything already there, so every insert turns right, and the tree grows into a single drooping line:

1
 \
  2
   \
    3
     \
      4
       \
        5

That is a linked list wearing a tree costume. Searching for 5 visits every node — height is n, search is O(n). The ordering rule is perfectly intact; it is the performance that is gone. And this is no contrived case: sorted data, reverse-sorted data, and many real access patterns produce exactly this shape. The cure is to keep the tree height-balanced, so that for n nodes the height stays near log₂ n — about 17 levels for a million nodes, not a million.

What a rotation does

A self-balancing tree adds one extra rule on top of the BST rule, and whenever an insert or delete breaks it, the tree repairs itself with a rotation — a tiny, local relinking that changes height without disturbing the order. Picture a left-heavy subtree, and a right rotation that lifts its left child up:

BEFOREAFTERyxCABright-rotate yxAyBC

Just three pointers move. The in-order sequence A, x, B, y, C is unchanged — so the BST rule still holds, while a left-heavy subtree becomes even.

Read the order off both pictures: A, x, B, y, C, before and after. The values keep their relation, so search still works — but the subtree got one level shorter. That is the whole mechanical trick: no recopying, no rebuilding, just relinking three pointers. Two families of tree use it:

  • An AVL tree keeps the two sides of every node within one level of each other, rotating eagerly to stay strictly even — best when reads dominate.
  • A red-black tree uses a looser colouring rule, so it rotates less per insert. It backs most standard-library ordered maps — Java’s TreeMap, C++‘s std::map.

You rarely build these yourself; you reach for the library. But seeing the rotation makes clear why the library can promise O(log n) no matter what order the data arrives.

Watching the collapse in code

Let us measure the damage directly — insert 1 through 15 in sorted order into a plain BST and check its height against what a balanced tree would give:

import math

class Node:
    def __init__(self, val):
        self.val = val
        self.left = self.right = None

def insert(root, val):
    if root is None:
        return Node(val)
    if val < root.val:
        root.left = insert(root.left, val)
    else:
        root.right = insert(root.right, val)
    return root

def height(node):
    if node is None:
        return 0
    return 1 + max(height(node.left), height(node.right))

root = None
for i in range(1, 16):                 # 1..15, already sorted — the worst input
    root = insert(root, i)

print("nodes inserted          :", 15)
print("actual height           :", height(root))
print("a balanced tree would be:", math.ceil(math.log2(16)))
nodes inserted          : 15
actual height           : 15
a balanced tree would be: 4

Height 15 where 4 was possible — a search for the last value now visits all fifteen nodes. That gap is exactly what AVL and red-black trees rotate away.

B-trees: trading depth for width

Self-balancing BSTs fix the problem in memory. Databases face a different enemy: the data lives on disk, and a single disk read costs thousands of times more than a memory access — so the goal becomes minimising reads, not comparisons.

A binary tree of a million nodes is about 20 levels deep, and if each node sits on its own disk page, that is up to 20 reads to find one row. A B-tree flattens this by widening each node: instead of two children, a node holds hundreds of keys and hundreds of child pointers, sized to fill exactly one disk page.

         [30 | 70]
        /     |     \
 [10|20]   [40|50|60]   [80|90]

That toy has fan-out 3; real indexes use fan-out in the hundreds. A B-tree only 3 or 4 levels tall can index tens of millions of rows, and each level is one disk read. Databases almost always use the B+-tree variant, where the actual records live only in the leaves, internal nodes are pure routing, and the leaves are chained in sorted order — so a range query like WHERE price BETWEEN 10 AND 50 finds the start and then just walks the leaf chain.

Practice

Quick check

0/3
Q1You insert 10, 20, 30, 40, 50 into a plain BST in that order. What is the height?
Q2A rotation changes a self-balancing tree's shape. What does it always preserve?
Q3A database index on 10 million rows needs only about 3-4 disk reads to find a row. The main reason?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
How does a B-tree index work, and when does the database choose not to use it?

A B-tree index stores key values in a balanced tree of sorted nodes, allowing the engine to reach any value in O(log n) page reads instead of scanning every row. The optimizer skips the index when the estimated cost of random I/O exceeds a full-table scan, when a function wraps the indexed column, or when the query returns such a large fraction of rows that a sequential scan is cheaper.

Return the level-order (BFS) traversal of a binary tree as a list of lists, one per level.

Use a queue (deque) to process nodes layer by layer. At each step, snapshot the current queue length to know exactly how many nodes belong to the current level, drain those, then enqueue their children. The result is a list of lists without any depth-tracking variable.

Search for a target in a rotated sorted array in O(log n).

Even after rotation, one of the two halves around mid is always fully sorted. Check which half is sorted, then decide whether the target falls inside it. If yes, narrow to that half; if no, search the other. This keeps binary search's O(log n) guarantee.

Implement binary search correctly — and explain the off-by-one traps.

Binary search halves the search space each iteration to find a target in O(log n). The tricky part is not the idea but the boundary conditions: closed vs. half-open intervals, how to update lo/hi, and when to use lo < hi vs. lo <= hi. One clean template eliminates all the classic bugs.

Related lessons

Explore further

Skip to content