Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Balanced Trees  ›  Lesson

Balance by Coin Flip

Treaps & Skip Lists 28 minA treap keeps the BST shape but bubbles the highest random priority to the top with the same rotations as lesson 1; a skip list reaches the same O(log n) search with shorter express lanes built by a coin flip at insert time
You're building a piece ofQuarry — the index layer
This piece — treap_insert_sequence(): Balances by random priority instead of bookkeeping, with the same O(log n) guarantee on average.
Scenario Quarry's index layer wants O(log n) inserts without tracking a balance factor on every node — a treap gets there by paying a coin flip instead.
Your task
Build treap_insert_sequence(values, seed): insert `values` one at a time into a treap, drawing each new node's random priority from random.Random(seed).random() (one priority per insert, in insertion order, so the SAME rng object keeps producing fresh draws), and rebalancing with rotations whenever a child's priority beats its parent's. Return {"inorder": [...], "height": ..., "priorities": [...]} where priorities are rounded to 4 decimal places, in the order they were drawn. Example: treap_insert_sequence([1, 2, 3, 4, 5, 6, 7, 8], 42) -> {"inorder": [1, 2, 3, 4, 5, 6, 7, 8], "height": 5, "priorities": [0.6394, 0.025, 0.275, 0.2232, 0.7365, 0.6767, 0.8922, 0.0869]}.

Balance by Coin Flip

In short: the last two lessons kept a tree balanced by bookkeeping — a balance factor, four rotation cases, a parent-child colour rule. A treap gets O(log n) height a different way: give every node a random priority when it's inserted, and keep the highest priority at the top of every subtree, like a heap — using the exact same rotations from lesson 1, just triggered by a coin flip instead of a balance check. A skip list reaches the same O(log n) search cost by building shorter "express lane" linked lists above the full list, one promotion coin flip per insert, so a search drops down a level every time it would overshoot. Both trade bookkeeping for randomness, and both guarantee O(log n) on average, not in the worst case — an unlucky run of coin flips can still produce a chain.

Three stacked horizontal lanes, the top with 3 stops, the middle with 6, the bottom with every item — one path drops from the top lane to the middle lane, then to the bottom lane, each drop happening where the next stop would overshoot the target

A matchmaking queue sorted by rating

A matchmaking service indexes waiting players by skill rating so it can quickly find "closest rating available." Players queue and leave in an order nobody controls — sometimes a burst of similarly-rated players joins at once, which is exactly the pattern that degrades a plain BST. A treap keeps the index short without checking a balance factor on every insert:

python

Height 5 for 8 ratings, inserted in the exact ascending order that gave lesson 1's plain BST a height of 8. Nothing checked a balance factor — the shape came entirely from which node happened to draw the highest priority. A genuinely unlucky sequence of coin flips could still produce a chain, but across many inserts that's as unlikely as flipping 8 heads in a row.

Bubbling a new root to the top

Here's a small example, drawn by hand, of what one insert's rotations actually look like. Insert 5, 2, 8, 1 into an empty treap with seed 42, then insert 9:

watch it move · tree

A treap after inserting 5, 2, 8, 1 with seed 42. Priorities, drawn in that order, are 0.6394, 0.025, 0.275, 0.2232 — 5 is on top only because it drew the highest priority so far.

Every node carries the random priority it drew on insert. A treap keeps the HIGHEST priority on top of each subtree, like a max-heap. Binary tree, 4 nodes, height 3.

1 / 7

An in-memory session index, searched often

A server keeps active sessions in a sorted structure so it can answer "does this token exist?" fast. A skip list builds several linked lists over the same sorted data: the bottom list has every session, and each list above it has roughly half as many, picked by a coin flip at insert time — an express lane.

python
step through it
1import random
2 
3class SkipNode:
4 def __init__(self, v, levels):
5 self.v = v
6 self.next = [None] * levels
7 
8class SkipList:
9 def __init__(self, seed, p=0.5, max_level=4):
10 self.rng = random.Random(seed)
11 self.p = p
12 self.max_level = max_level
13 self.head = SkipNode(None, max_level)
14 self.level = 1
15 
16 def random_level(self):
17 lvl = 1
18 while self.rng.random() < self.p and lvl < self.max_level:
19 lvl += 1
20 return lvl
21 
22 def insert(self, v):
23 lvl = self.random_level()
24 self.level = max(self.level, lvl)
25 node = SkipNode(v, lvl)
26 update = [self.head] * self.max_level
27 cur = self.head
28 for i in range(self.level - 1, -1, -1):
29 while cur.next[i] is not None and cur.next[i].v < v:
30 cur = cur.next[i]
31 update[i] = cur
32 for i in range(lvl):
33 node.next[i] = update[i].next[i]
34 update[i].next[i] = node
35 return lvl
36 
37 def search(self, v):
38 cur = self.head
39 hops = 0
40 for i in range(self.level - 1, -1, -1):
41 while cur.next[i] is not None and cur.next[i].v < v:
42 cur = cur.next[i]
43 hops += 1
44 cur = cur.next[0]
45 return cur is not None and cur.v == v, hops
46 
47sl = SkipList(seed=42)
48levels = [sl.insert(v) for v in range(1, 21)]
49print("levels assigned, in insert order:", levels)
50print("search for 17:", sl.search(17))
51print("search for 5:", sl.search(5))

The mistake people actually make: writing the search to only ever walk next[0], the bottom lane. That's a correct linked list, but it throws away every express lane — search degrades straight back to O(n). The whole point is walking the top lane first and only dropping down when the next stop would overshoot.

What it costs

structure search / insert why worst case
treap O(log n) expected priorities are drawn uniformly at random, so no key order is "bad" O(n), an unlucky run of priorities
skip list O(log n) expected each level has about half the nodes of the one below, by a fair coin flip O(n), an unlucky run of promotions
AVL / red-black (lessons 1–2) O(log n) guaranteed bookkeeping fixes every violation immediately never degrades

The trade: treaps and skip lists are simpler to implement correctly (no four-case rotation table, no recolouring rules) and support ranges and concurrent access well, in exchange for a worst case that bookkeeping-based trees don't have.

Quick practice

What does this print?
def would_rotate(parent_priority, child_priority):
    return child_priority > parent_priority

print(would_rotate(0.8, 0.3), would_rotate(0.3, 0.8))
broken — fix it

This prints 16 (a full linear scan) instead of 5. Rewrite `hops_to_find` to walk the levels from `self.level - 1` down to `0`, the same loop shape as `search`, counting a hop for every `cur = cur.next[i]` at ANY level.

🎯 Your turn

Build treap_insert_sequence(values, seed): insert values one at a time into a treap, drawing each new node's priority from random.Random(seed) (created once, one draw per insert, in order), and rebalance with rotations whenever a child's priority beats its parent's. Return {"inorder": [...], "height": ..., "priorities": [...]}, with priorities rounded to 4 decimal places in the order they were drawn.

  • treap_insert_sequence([1, 2, 3, 4, 5, 6, 7, 8], 42) → {"inorder": [1, 2, 3, 4, 5, 6, 7, 8], "height": 5, "priorities": [0.6394, 0.025, 0.275, 0.2232, 0.7365, 0.6767, 0.8922, 0.0869]}

Hint: reuse rotate_left/rotate_right from lesson 1 unchanged. Insert by key like a plain BST, then on the way back up compare the child's priority to its parent's and rotate if the child wins.

Press ▶ Run, try the Live App chip, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upThe first coin flip

Write first_priority(seed): create random.Random(seed) and return the first priority it draws with .random(), rounded to 4 decimal places.

randomised-algorithms
DrillDoes the heap property break?

Write would_rotate(parent_priority, child_priority): return True if a child with child_priority would need to rotate up past a parent with parent_priority (the heap property is violated), else False. Equal priorities do not rotate.

balanced-trees
BuildJust the height

Write treap_height(values, seed): build a treap from values with priorities drawn from random.Random(seed), the same way as the lesson, and return just its final height.

balanced-treesrandomised-algorithms
BossHow many rotations did it cost?

Write rotation_count(values, seed): build a treap from values the same way as the lesson, and return the total number of rotations performed across every insert.

balanced-treesrandomised-algorithms
CapstoneWhat the coin flip actually buys

Write treap_vs_plain_bst(n, seed): build a treap and a plain (unbalanced) BST from the SAME sorted keys list(range(n)), inserted in that ascending order, using priorities from random.Random(seed) for the treap. Return {"treap_height": ..., "bst_height": ...}.

balanced-treescomplexity
treap_insert_sequence.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
treap_insert_sequence(values, seed) → dictThe in-order sequence, final height, and the random priorities drawn for each insert, of a treap built from values.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.