Balanced Trees and Ordered Sets
A short tree on purpose
Introduction to Binary Search Trees ended on a chain: insert sorted values and every one hangs below the last, so the height, and with it every search, grows to n. The fix is one small move that changes a tree's shape without moving any value out of order: a rotation. Balanced trees rotate whenever a path gets too long, which keeps the height near log n, and the sorted sets and maps your language gives you are such trees:

Rotations
Take a node, 6, and its left child, 3. Everything in 3's left subtree is below 3, everything in its right subtree is between 3 and 6, and everything on 6's right is above 6. A right rotation lifts 3 into 6's place: 6 becomes 3's right child, and 3's right subtree, the values between them, moves across to become 6's left. Every value is still on the correct side of every node, so the inorder is exactly the same. A left rotation is the mirror. Each one is three links, O(1):
// Lift node's left child into its place and return it: its right subtree moves across.
TreeNode* rotateRight(TreeNode* node) {
TreeNode* top = node->left;
node->left = top->right; // between top and node, so it stays between them
top->right = node;
return top;
}
// The mirror: lift node's right child.
TreeNode* rotateLeft(TreeNode* node) {
TreeNode* top = node->right;
node->right = top->left;
top->left = node;
return top;
}
def rotate_right(node):
"""Lift node's left child into its place and return it: its right subtree moves across."""
top = node.left
node.left = top.right # between top and node, so it stays between them
top.right = node
return top
def rotate_left(node):
"""The mirror: lift node's right child."""
top = node.right
node.right = top.left
top.left = node
return top
// Lift node's left child into its place and return it: its right subtree moves across.
static TreeNode rotateRight(TreeNode node) {
TreeNode top = node.left;
node.left = top.right; // between top and node, so it stays between them
top.right = node;
return top;
}
// The mirror: lift node's right child.
static TreeNode rotateLeft(TreeNode node) {
TreeNode top = node.right;
node.right = top.left;
top.left = node;
return top;
}
// Lift node's left child into its place and return it: its right subtree moves across.
function rotateRight(node) {
const top = node.left;
node.left = top.right; // between top and node, so it stays between them
top.right = node;
return top;
}
// The mirror: lift node's right child.
function rotateLeft(node) {
const top = node.right;
node.right = top.left;
top.left = node;
return top;
}
Each printed [2, 1, 3] for the chain [1, null, 2, null, 3] rotated left, so 2 rises and the height drops from 2 to 1, and [3, 1, 6, null, null, 4, 7] for [6, 3, 7, 1, 4] rotated right: 3 rises, and 4, which sat between 3 and 6, is now 6's left child.
Below, click a node to lift it over its parent. The inorder under the tree never changes; the height does. Get the chain down to the least height its values allow, or ask the hint, which lifts the middle value of each subtree to its top.

When to rotate: AVL and red-black trees
A balanced tree is a binary search tree with a rule about its shape, checked after every insert and delete and restored by rotations. You won't write one in an interview, but you should know the two rules:
- AVL trees keep, at every node, the heights of its two subtrees within 1 of each other. After an insert, walk back up the path; the first node out of balance is fixed with one rotation when the new value went down a straight line (left then left), and with two when the path bends (left then right): the first rotation straightens the bend, the second lifts the middle value to the top.
- Red-black trees colour every node red or black: the root is black, a red node never has a red child, and every path from a node down to an empty place passes the same number of black nodes. So the longest path is at most twice the shortest, and the height stays under 2 log₂(n + 1). Fixing the colours takes at most a few rotations per insert.
Step through an AVL tree taking 1 to 7 in order, the input that made a chain. Each rotation lifts the middle of a straight line, and the tree never gets more than one level out of balance:
Inserting 1 to 7 into an AVL tree — Indigo is the value just inserted and orange the node out of balance; in a rotation, green rises over amber.
When the path to the new value bends instead, one rotation isn't enough:

Measured on sorted inserts, the worst case for a plain tree: the plain tree's height is n − 1, while the AVL tree's stays near log₂ n, 16 for 100,000 values:
| Sorted values | Plain height | AVL height | Plain / AVL time |
|---|---|---|---|
| 1,000 | 999 | 9 | 48.8 ms / 11.7 ms |
| 2,000 | 1,999 | 10 | 109.8 ms / 23.6 ms |
| 4,000 | 3,999 | 11 | 432.9 ms / 51.5 ms |
Java's TreeMap and TreeSet are red-black trees, and so, in practice, are C++'s map and set. When a problem needs sorted data that changes, use them instead of writing one.
Ordered sets in your language
Each ordered set answers the two questions that come up most, the floor (the largest key at or below x) and the ceiling (the smallest key at or above x):
| Language | Sorted set and map | Floor and ceiling of x | Insert |
|---|---|---|---|
| Java | TreeSet, TreeMap | floor(x), ceiling(x); floorKey, ceilingKey on a map | O(log n) |
| C++ | set, map | the key before upper_bound(x); lower_bound(x) | O(log n) |
| Python | none built in: a sorted list | a[bisect_right(a, x) - 1]; a[bisect_left(a, x)] | O(n), the list shifts |
| JavaScript | none built in: a sorted array | your own binary search | O(n), the array shifts |
- Java:
floorandceilingreturnnullwhen there is no such key; keep theInteger, check it, then unbox.lowerandhigherare the strict versions. - C++:
lower_bound(x)is the first key not below x, which is the ceiling, not the floor. The floor is the key just beforeupper_bound(x), and only if that isn'tbegin(). - Python:
bisect_rightfor the floor (it passes keys equal to x),bisect_leftfor the ceiling.insortkeeps a list sorted, but the insert shifts everything after it. - JavaScript: there's no sorted container, and
sort()with no comparator compares numbers as strings: pass(a, b) => a - b.
// The largest key at or below x, or "none": the one just before the first key above x.
string floorOf(const set<int>& keys, int x) {
auto it = keys.upper_bound(x); // the first key bigger than x
return it == keys.begin() ? "none" : to_string(*prev(it));
}
// The smallest key at or above x, or "none".
string ceilingOf(const set<int>& keys, int x) {
auto it = keys.lower_bound(x); // the first key not below x
return it == keys.end() ? "none" : to_string(*it);
}
from bisect import bisect_left, bisect_right
def floor_of(keys, x):
"""The largest of the sorted keys at or below x, or None."""
i = bisect_right(keys, x) # keys[:i] are all at or below x
return keys[i - 1] if i else None
def ceiling_of(keys, x):
"""The smallest of the sorted keys at or above x, or None."""
i = bisect_left(keys, x) # keys[i:] are all at or above x
return keys[i] if i < len(keys) else None
// TreeSet answers both directly; each returns null when there is no such key.
static String floorOf(TreeSet<Integer> keys, int x) {
Integer f = keys.floor(x);
return f == null ? "none" : f.toString();
}
static String ceilingOf(TreeSet<Integer> keys, int x) {
Integer c = keys.ceiling(x);
return c == null ? "none" : c.toString();
}
// No sorted set is built in, so binary-search a sorted array. lo ends at the first key above x.
function floorOf(keys, x) {
let lo = 0, hi = keys.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (keys[mid] <= x) lo = mid + 1; else hi = mid;
}
return lo > 0 ? keys[lo - 1] : "none";
}
// Here lo ends at the first key at or above x.
function ceilingOf(keys, x) {
let lo = 0, hi = keys.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (keys[mid] < x) lo = mid + 1; else hi = mid;
}
return lo < keys.length ? keys[lo] : "none";
}
Each printed 20 30 none none 30 for the keys 10, 20, 30, 40: the floor and ceiling of 25, no floor of 5, no ceiling of 45, and 30 as its own floor.
A sorted list's O(n) insert is fast at first, because the shift is one block move in memory, but it grows with the list. Inserting random values one at a time:
| Values | Python, sorted list and insort | Java, TreeSet |
|---|---|---|
| 50,000 | 466 ms | 65 ms |
| 100,000 | 1,783 ms | 108 ms |
| 200,000 | 6,945 ms | 277 ms |
Python's time grew 15 times for 4 times the values, the O(n²) of n shifting inserts; Java's 4 times. At a few thousand inserts the list is fine; at 100,000 it already took 1,783 ms. Past that, Python solutions sort once at the end when they can, or use a third-party sorted container where the judge has one.
Booking with a floor and a ceiling
My Calendar I books [start, end) only if it overlaps nothing booked already. The bookings never overlap each other, so sorted by start they're sorted by end too, and a new booking can only clash with two of them: its floor, the last booking that starts at or before start, which must end by start, and its ceiling, the first one that starts after start, which must begin at or after end:

class MyCalendar {
map<int, int> booked; // start → end, sorted by start
public:
MyCalendar() {}
bool book(int start, int end) {
auto after = booked.upper_bound(start); // the first booking starting after start
if (after != booked.end() && after->first < end) return false;
if (after != booked.begin() && prev(after)->second > start) return false; // the last one at or before start
booked[start] = end;
return true;
}
};
from bisect import bisect_right
class MyCalendar:
def __init__(self):
self.starts = [] # the bookings' starts, sorted
self.ends = [] # each start's end, at the same index
def book(self, start: int, end: int) -> bool:
i = bisect_right(self.starts, start) # the bookings before i start at or before start
if i > 0 and self.ends[i - 1] > start: # the floor still runs at start
return False
if i < len(self.starts) and self.starts[i] < end: # the ceiling begins before end
return False
self.starts.insert(i, start) # O(n): the list shifts
self.ends.insert(i, end)
return True
class MyCalendar {
private final TreeMap<Integer, Integer> booked = new TreeMap<>(); // start → end, sorted by start
public MyCalendar() {}
public boolean book(int start, int end) {
Map.Entry<Integer, Integer> floor = booked.floorEntry(start); // the last booking starting at or before start
if (floor != null && floor.getValue() > start) return false;
Integer ceiling = booked.ceilingKey(start); // the first booking starting at or after start
if (ceiling != null && ceiling < end) return false;
booked.put(start, end);
return true;
}
}
var MyCalendar = function() {
this.starts = []; // the bookings' starts, sorted
this.ends = []; // each start's end, at the same index
};
MyCalendar.prototype.book = function(start, end) {
let lo = 0, hi = this.starts.length; // the first booking that starts after start
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.starts[mid] <= start) lo = mid + 1; else hi = mid;
}
if (lo > 0 && this.ends[lo - 1] > start) return false; // the floor still runs at start
if (lo < this.starts.length && this.starts[lo] < end) return false; // the ceiling begins before end
this.starts.splice(lo, 0, start); // O(n): the array shifts
this.ends.splice(lo, 0, end);
return true;
};
Each ran on the problem's own judge with its first example, book(10, 20), book(15, 25), book(20, 30), and returned [null, true, false, true]. In Java and C++ each booking is O(log n). The Python and JavaScript versions find the neighbours in O(log n) too, but their insert shifts the list; at this problem's 1,000 calls that costs nothing.
Spotting it, and when it breaks
The clues. "The largest value at or below", "the closest", "the next bigger", "is there an overlap with what's booked", over data that keeps changing: an ordered set's floor and ceiling. "Sorted, with inserts and deletes": a balanced tree, which your language's ordered set already is.
The near-misses. Data that doesn't change: sort it once and binary-search an array (Binary Search). Only the smallest or the largest, never a floor: a heap (Heaps). Exact lookups with no order: a hash set (Hashing).
When it breaks. A plain binary search tree on sorted or nearly sorted input becomes a chain. A sorted list's O(n) insert dominates by 10^5 inserts (1,783 ms in Python). TreeSet and set keep one copy of each value: count duplicates in a TreeMap or a multiset.
Bugs that cost a submission
Unboxing a missing floor (Java).
static int floorOf(TreeSet<Integer> keys, int x) {
int f = keys.floor(x); // null when nothing is at or below x
return f;
}
For the floor of 5 in {10, 20} it threw a NullPointerException: there is no key at or below 5, so floor returned null. Keep the Integer and check it.
lower_bound as the floor (C++).
int floorOf(const set<int>& keys, int x) {
return *keys.lower_bound(x); // the first key NOT BELOW x: that's the ceiling
}
For 25 in {10, 20, 30, 40} it returned 30, the ceiling; the floor is 20. Step back from upper_bound instead, checking for begin().
bisect_left for the floor (Python).
from bisect import bisect_left
def floor_of(keys, x):
i = bisect_left(keys, x) # stops before a key equal to x
return keys[i - 1] if i else None
For 30 in [10, 20, 30, 40] it returned 20: bisect_left stops before the 30 itself. The floor uses bisect_right.
Sorting numbers with the default sort (JavaScript).
const keys = [10, 9, 100, 25];
keys.sort(); // compares them as strings
console.log(keys.join(" "));
It printed 10 100 25 9, string order, and every binary search on that array is wrong. Pass (a, b) => a - b.
Check yourself
Quiz
1 / 6Multiple choice
Multiple choice
Multiple choice
Multiple choice
Multiple choice
lower_bound(25) return for the set {10, 20, 30, 40}?Multiple choice
bisect.insort cost on a sorted list of n values?Practice problems
- My Calendar I: one floor and one ceiling per booking.
Next, Binary Search Trees Quiz: the whole section in sixteen questions.