← Data structures & algorithms
Lookup Answer questions about the keys nearby

Ordered map

Find the chapter at the playhead, and the next one.

Drag the scrubber on a video with chapters and the title above it changes as you cross each one. The player is not asking “what starts at 25:00?”; almost nothing does. It asks which chapter started most recently before 25:00, and where the next one begins. Redis answers the same kind of question for leaderboards with its sorted sets, and a database index does it for every WHERE start BETWEEN. Let’s edit a podcast’s chapter markers with a map that keeps its keys in order.

TypeScriptGoOne chapter list, two implementations.

01 / The idea

Ask about the keys nearby.

A hash map is superb at one question: what is stored under exactly this key? A podcast editor asks different ones. Which chapter is playing at 25:00? Where does Next chapter jump? Is 24:15 too close to the chapter before it? Which markers belong on the visible stretch of the timeline? Every one is about the keys near a value, and a hash scatters nearby keys on purpose.

An ordered map keeps its keys sorted, so it can answer them directly. Its core questions have names: the floor of a value is the largest key at or below it, the ceiling the smallest key at or above it, and a range is every key from one value up to another.

You rely on them already. Redis documents sorted sets as “a dual-ported data structure containing both a skip list and a hash table,” with leaderboards as a common use, and ZRANGEBYSCORE costs “O(log(N)+M)”. PostgreSQL’s B-tree indexes “can handle equality and range queries on data that can be sorted.” YouTube’s chapters have rules an ordered map enforces neatly: the first starts at 00:00, and each lasts at least 10 seconds.

02 / Name the rule

Floor, ceiling, and range, from one search.

All three start by finding where a value would sit among the keys. The chapter playing at the playhead is its floor. Next chapter is the ceiling of one millisecond later. The markers in view are a range: find the first at or after the view’s start, then read in order until the end of the view.

This lesson builds the map as a skip list, which William Pugh proposed in 1989. Every key sits on the bottom level, a sorted linked list. Each key also flips a coin: heads, and it appears on the level above too, then flips again. About half the keys reach level 2, a quarter level 3. A search runs along the top level while the next key is still smaller, drops a level, and repeats, so it skips most keys on the way down: O(log n) moves expected, whatever order the keys arrived in.

The coin makes the shape random but not the answers. Every floor, ceiling, and range is exact; only the number of moves varies. This lesson seeds the coin so both languages build the same towers.

Why a skip list, and not a balanced binary search tree?The same questions, a different trade

A balanced search tree answers floor, ceiling, and range too, in guaranteed O(log n). A skip list trades that guarantee for simplicity: an insert links a tower in and never rotates. That makes it friendly to concurrency. Java ships both, TreeMap and ConcurrentSkipListMap, and LevelDB’s skip list lets reads “progress without any internal locking or synchronization.” RocksDB’s default memtable is a skip list.

The cost is pointer-chasing: in the counts below, the skip list made about 31 moves to find one of 100,000 keys, where a balanced tree needs about log₂ n, 17, comparisons. Neither is the right choice for a dozen chapters, as the cost section says.

03 / Follow one operation

Five chapters, one episode.

A 42-minute episode has five chapters: Cold open, Welcome, The interview at 07:45, Listener questions at 24:10, and Wrap-up at 38:20. The playhead is at 25:00. An editor tries to add a sponsor read at 24:15, adds a tangent at 18:00, looks at the timeline from 05:00 to 20:00, and cuts three minutes.

Before you watch, predict which chapter is playing at 25:00 and how many moves it takes, and what the cut does to the chapters after it. The animation replays what the TypeScript example recorded. Try it lets you edit the episode yourself.

Ordered map

Skip ahead, drop down, land in order.

Skip list · each chapter is a tower; a level links every tower that reaches it

Episode 42:00

Towers in time order: 00:00 Cold open, 2 levels; 01:30 Welcome, 1 levels; 07:45 The interview, 1 levels; 24:10 Listener questions, 2 levels; 38:20 Wrap-up, 1 levels.

  1. 00:00 Cold open
  2. 01:30 Welcome
  3. 07:45 The interview
  4. 24:10 Listener questions
  5. 38:20 Wrap-up

Chapters 5 Levels 2 Moves 0

01/ 07
Five chapters, two levels

Five chapters, two levels.

Every chapter sits on the bottom level in time order. Cold open and Listener questions also reached level 2 on their coin flips, so a search can skip from 00:00 straight to 24:10.

Reduced motion: choose a scene to see its completed state.

Read this scene

Every chapter sits on the bottom level in time order. Cold open and Listener questions also reached level 2 on their coin flips, so a search can skip from 00:00 straight to 24:10.

Every chapter sits on the bottom level in time order. Cold open and Listener questions also reached level 2 on their coin flips, so a search can skip from 00:00 straight to 24:10.

Chapters: 5. Levels: 2. Moves so far: 0.

Watch restarts when you return. Step through keeps your selected step. Try it starts from the episode’s five chapters each time you open it.

04 / Read the shape

One skip list, one chapter list.

Basic form is SkipListMap: set, get, delete, floor, ceiling, and range over integer keys, recording every move, link, and read. In the wild wraps it in ChapterList, which finds the chapter playing and the next and previous ones, keeps chapters at least 10 seconds long, lists the markers in a view, cuts a section, and writes description lines.

An ordered map over integer keys, built as a skip list with seeded coin flips. Set, get, delete, floor, ceiling, and range, recording every move right, drop, link, unlink, and read.

TypeScriptReading
chapters.ts
export type Step = {
	/**
	 * right: move along a level to this node, whose key is still smaller. down: drop a level at
	 * this node, or at the head when `at` is null. found: the key is here. link or unlink: add
	 * or remove this node on one level. visit: read a node in key order.
	 */
	kind: 'right' | 'down' | 'found' | 'link' | 'unlink' | 'visit';
	at: number | null;
	level: number;
};

export type Entry<V> = { key: number; value: V };
type Node<V> = { key: number; value: V; next: (Node<V> | null)[] };

const MAX_LEVEL = 16;
const SEED = 2463534242;

function checkKey(key: number) {
	if (!Number.isSafeInteger(key)) throw new RangeError('key must be a safe integer');
}

// An ordered map keeps its keys sorted, so it can answer more than "what is stored here":
// the nearest key at or below a value, the nearest at or above, and every key in a range.
// This one is a skip list. Every key sits on the bottom level in order; about half also sit
// on the level above, a quarter on the next, and so on, decided by coin flips. A search runs
// along the top level while the next key is still smaller, then drops a level and repeats,
// skipping most keys on the way down.
export class SkipListMap<V> {
	#head: Node<V> = { key: 0, value: undefined as V, next: Array(MAX_LEVEL).fill(null) };
	#level = 1;
	#size = 0;
	// The coin: xorshift32 from a fixed seed, so every run, and the Go version, flips the same.
	#state = SEED;

	set(key: number, value: V, steps?: Step[]): boolean {
		checkKey(key);
		const { update, next } = this.#search(key, steps);
		if (next?.key === key) {
			steps?.push({ kind: 'found', at: key, level: 0 });
			next.value = value;
			return false;
		}
		const height = this.#randomHeight();
		this.#level = Math.max(this.#level, height);
		const node: Node<V> = { key, value, next: Array(height).fill(null) };
		for (let level = 0; level < height; level++) {
			node.next[level] = update[level].next[level];
			update[level].next[level] = node;
			steps?.push({ kind: 'link', at: key, level });
		}
		this.#size++;
		return true;
	}

	get(key: number, steps?: Step[]): V | undefined {
		checkKey(key);
		const { next } = this.#search(key, steps);
		if (next?.key !== key) return undefined;
		steps?.push({ kind: 'found', at: key, level: 0 });
		return next.value;
	}

	delete(key: number, steps?: Step[]): boolean {
		checkKey(key);
		const { update, next } = this.#search(key, steps);
		if (!next || next.key !== key) return false;
		steps?.push({ kind: 'found', at: key, level: 0 });
		for (let level = 0; level < next.next.length; level++) {
			update[level].next[level] = next.next[level];
			steps?.push({ kind: 'unlink', at: key, level });
		}
		while (this.#level > 1 && !this.#head.next[this.#level - 1]) this.#level--;
		this.#size--;
		return true;
	}

	/** The entry with the largest key at or below `key`, or null. */
	floor(key: number, steps?: Step[]): Entry<V> | null {
		checkKey(key);
		const { update, next } = this.#search(key, steps);
		if (next?.key === key) {
			steps?.push({ kind: 'found', at: key, level: 0 });
			return { key, value: next.value };
		}
		const below = update[0];
		return below === this.#head ? null : { key: below.key, value: below.value };
	}

	/** The entry with the smallest key at or above `key`, or null. */
	ceiling(key: number, steps?: Step[]): Entry<V> | null {
		checkKey(key);
		const { next } = this.#search(key, steps);
		if (!next) return null;
		if (next.key === key) steps?.push({ kind: 'found', at: key, level: 0 });
		return { key: next.key, value: next.value };
	}

	/** Up to `limit` entries with `from <= key < to`, in key order. */
	range(from: number, to: number, limit: number, steps?: Step[]): Entry<V>[] {
		checkKey(from);
		checkKey(to);
		if (!Number.isInteger(limit) || limit < 1 || limit > 10_000)
			throw new RangeError('limit must be a whole number from 1 to 10000');
		const found: Entry<V>[] = [];
		let node = this.#search(from, steps).next;
		for (; node && node.key < to && found.length < limit; node = node.next[0]) {
			steps?.push({ kind: 'visit', at: node.key, level: 0 });
			found.push({ key: node.key, value: node.value });
		}
		return found;
	}

	/** Every key in order with the height of its tower, for drawing. */
	towers(): { key: number; height: number }[] {
		const found: { key: number; height: number }[] = [];
		for (let node = this.#head.next[0]; node; node = node.next[0])
			found.push({ key: node.key, height: node.next.length });
		return found;
	}

	get size(): number {
		return this.#size;
	}

	/** Levels in use: the tallest tower, and at least 1. */
	get levels(): number {
		return this.#level;
	}

	// From the top level down, move right while the next key is smaller. `update` remembers
	// the last node on each level, which is where a new node would be linked in.
	#search(key: number, steps?: Step[]) {
		const update: Node<V>[] = Array(MAX_LEVEL).fill(this.#head);
		let node = this.#head;
		for (let level = this.#level - 1; level >= 0; level--) {
			for (let next = node.next[level]; next && next.key < key; next = node.next[level]) {
				node = next;
				steps?.push({ kind: 'right', at: node.key, level });
			}
			update[level] = node;
			if (level > 0)
				steps?.push({ kind: 'down', at: node === this.#head ? null : node.key, level });
		}
		return { update, next: node.next[0] };
	}

	// Each head on the coin raises the tower one more level, so about half the keys reach
	// level 2, a quarter level 3, and so on.
	#randomHeight(): number {
		let height = 1;
		while (height < MAX_LEVEL && (this.#flip() & 1) === 1) height++;
		return height;
	}

	#flip(): number {
		let x = this.#state;
		x ^= x << 13;
		x ^= x >>> 17;
		x ^= x << 5;
		this.#state = x >>> 0;
		return this.#state;
	}
}
GoAlongside
chapters.go
// Step is one move. Kind is "right" (move along a level to this node, whose key is still
// smaller), "down" (drop a level at this node, or at the head when Head is true), "found"
// (the key is here), "link" or "unlink" (add or remove this node on one level), or "visit"
// (read a node in key order).
type Step struct {
	Kind  string `json:"kind"`
	At    int    `json:"at"`
	Head  bool   `json:"head,omitempty"`
	Level int    `json:"level"`
}

type Entry[V any] struct {
	Key   int
	Value V
}

type Tower struct {
	Key, Height int
}

type node[V any] struct {
	key   int
	value V
	next  []*node[V]
}

const (
	maxLevel = 16
	seed     = 2463534242
)

func record(steps *[]Step, step Step) {
	if steps != nil {
		*steps = append(*steps, step)
	}
}

// SkipMap is an ordered map: it keeps its keys sorted, so it can answer more than "what is
// stored here": the nearest key at or below a value, the nearest at or above, and every key
// in a range. It is a skip list. Every key sits on the bottom level in order; about half
// also sit on the level above, a quarter on the next, and so on, decided by coin flips. A
// search runs along the top level while the next key is still smaller, then drops a level
// and repeats, skipping most keys on the way down.
type SkipMap[V any] struct {
	head  *node[V]
	level int
	size  int
	// The coin: xorshift32 from a fixed seed, so every run, and the TypeScript version, flips the same.
	state uint32
}

func NewSkipMap[V any]() *SkipMap[V] {
	return &SkipMap[V]{head: &node[V]{next: make([]*node[V], maxLevel)}, level: 1, state: seed}
}

// Set returns false when the key was already there; its value is replaced.
func (m *SkipMap[V]) Set(key int, value V, steps *[]Step) bool {
	update, next := m.search(key, steps)
	if next != nil && next.key == key {
		record(steps, Step{Kind: "found", At: key})
		next.value = value
		return false
	}
	height := m.randomHeight()
	m.level = max(m.level, height)
	fresh := &node[V]{key: key, value: value, next: make([]*node[V], height)}
	for level := range height {
		fresh.next[level] = update[level].next[level]
		update[level].next[level] = fresh
		record(steps, Step{Kind: "link", At: key, Level: level})
	}
	m.size++
	return true
}

func (m *SkipMap[V]) Get(key int, steps *[]Step) (V, bool) {
	_, next := m.search(key, steps)
	if next == nil || next.key != key {
		var zero V
		return zero, false
	}
	record(steps, Step{Kind: "found", At: key})
	return next.value, true
}

func (m *SkipMap[V]) Delete(key int, steps *[]Step) bool {
	update, next := m.search(key, steps)
	if next == nil || next.key != key {
		return false
	}
	record(steps, Step{Kind: "found", At: key})
	for level := range next.next {
		update[level].next[level] = next.next[level]
		record(steps, Step{Kind: "unlink", At: key, Level: level})
	}
	for m.level > 1 && m.head.next[m.level-1] == nil {
		m.level--
	}
	m.size--
	return true
}

// Floor returns the entry with the largest key at or below key.
func (m *SkipMap[V]) Floor(key int, steps *[]Step) (Entry[V], bool) {
	update, next := m.search(key, steps)
	if next != nil && next.key == key {
		record(steps, Step{Kind: "found", At: key})
		return Entry[V]{key, next.value}, true
	}
	if below := update[0]; below != m.head {
		return Entry[V]{below.key, below.value}, true
	}
	return Entry[V]{}, false
}

// Ceiling returns the entry with the smallest key at or above key.
func (m *SkipMap[V]) Ceiling(key int, steps *[]Step) (Entry[V], bool) {
	_, next := m.search(key, steps)
	if next == nil {
		return Entry[V]{}, false
	}
	if next.key == key {
		record(steps, Step{Kind: "found", At: key})
	}
	return Entry[V]{next.key, next.value}, true
}

// Range returns up to limit entries with from <= key < to, in key order.
func (m *SkipMap[V]) Range(from, to, limit int, steps *[]Step) ([]Entry[V], error) {
	if limit < 1 || limit > 10_000 {
		return nil, errors.New("limit must be a whole number from 1 to 10000")
	}
	found := []Entry[V]{}
	_, n := m.search(from, steps)
	for ; n != nil && n.key < to && len(found) < limit; n = n.next[0] {
		record(steps, Step{Kind: "visit", At: n.key})
		found = append(found, Entry[V]{n.key, n.value})
	}
	return found, nil
}

// Towers lists every key in order with the height of its tower, for drawing.
func (m *SkipMap[V]) Towers() []Tower {
	found := []Tower{}
	for n := m.head.next[0]; n != nil; n = n.next[0] {
		found = append(found, Tower{n.key, len(n.next)})
	}
	return found
}

func (m *SkipMap[V]) Size() int { return m.size }

// Levels counts levels in use: the tallest tower, and at least 1.
func (m *SkipMap[V]) Levels() int { return m.level }

// search moves right on each level, from the top down, while the next key is smaller.
// update remembers the last node on each level, which is where a new node would be linked in.
func (m *SkipMap[V]) search(key int, steps *[]Step) ([maxLevel]*node[V], *node[V]) {
	var update [maxLevel]*node[V]
	for i := range update {
		update[i] = m.head
	}
	n := m.head
	for level := m.level - 1; level >= 0; level-- {
		for next := n.next[level]; next != nil && next.key < key; next = n.next[level] {
			n = next
			record(steps, Step{Kind: "right", At: n.key, Level: level})
		}
		update[level] = n
		if level > 0 {
			record(steps, Step{Kind: "down", At: n.key, Head: n == m.head, Level: level})
		}
	}
	return update, n.next[0]
}

// randomHeight raises the tower one more level for each head on the coin, so about half the
// keys reach level 2, a quarter level 3, and so on.
func (m *SkipMap[V]) randomHeight() int {
	height := 1
	for height < maxLevel && m.flip()&1 == 1 {
		height++
	}
	return height
}

func (m *SkipMap[V]) flip() uint32 {
	x := m.state
	x ^= x << 13
	x ^= x >> 17
	x ^= x << 5
	m.state = x
	return x
}
Reading the TypeScriptAn update array and a seeded coin

#search returns update, the last node it passed on every level, and the node after the target. set links a new tower in right after those nodes, delete unlinks from them, and floor reads update[0].

The coin is xorshift32 over a fixed seed, with >>> 0 keeping the state an unsigned 32-bit number, so the TypeScript and Go towers match exactly. Keys must be safe integers; a fractional or NaN key would never compare equal.

Reading the GoA fixed update array and a Head flag

search returns [maxLevel]*node[V], an array on the stack, so a lookup allocates nothing. uint32 arithmetic wraps on its own, which is exactly what xorshift needs.

Lookups return a value and a boolean. A Step marks a drop from the head with Head: true rather than a key, and the chapter list’s errors are values with the same messages as the TypeScript.

What would I normally use in application code?A sorted array, a database, or a library

Neither TypeScript nor Go ships an ordered map. For a podcast’s dozen chapters, keep a sorted array and use binary search for floor and ceiling; inserting shifts a few entries and nobody notices.

When the keys are many and keep changing, reach for what already exists: a sorted set in Redis, an indexed column with ORDER BY and a range in SQL, Java’s TreeMap or ConcurrentSkipListMap, or C++’s std::map with lower_bound.

05 / Try a decision

Next chapter, from exactly on a boundary.

Floor and ceiling include the value you ask about. That is right for some questions and wrong for others. Decide which lookup the button needs before the feedback tells you.

The playhead sits exactly on 24:10, where Listener questions starts, and the listener taps Next chapter. Which lookup should the button use?

06 / Follow the cost

31 moves for 100,000 keys.

Here is every operation at a glance, with n chapters. The rest of this section measures larger maps and an audiobook.

Chapter list: time and extra space
OperationTimeExtra spaceWhat it assumes
Chapter playingO(log n) expectedO(1)A floor lookup: move right while the next start is earlier, drop a level, and keep the last chapter passed.
Next chapterO(log n) expectedO(1)A ceiling lookup one millisecond past the playhead: the same search, then the chapter after it.
Add or remove a chapterO(log n) expectedO(1) expectedAdding looks at both neighbors first. A new tower averages two levels, so two links.
Markers in viewO(log n + m)O(m)Find the first marker at or after the view’s start, then read m along the bottom level.
Cut a sectionO((k + m) log n) expectedO(k + m)Take out the k chapters inside, then take out and put back each of the m later ones.
Sorted array insteadO(log n) lookup, O(n) addO(n)Binary search answers floor, ceiling, and range; adding or removing shifts the entries after it.
Hold n chapters—O(n) expectedOne tower per chapter. The coin gives each key about two forward pointers on average (2.001 measured on 100,000 keys), capped at 16 levels.

In the animation, finding the chapter at 25:00 took three moves among five chapters. With 1,000 keys added in shuffled order, finding a key averaged 17.06 moves, the longest 28, on 11 levels. With 100,000 keys it averaged 31.45 moves, the longest 58, on 16 levels, the cap. Sorted input made no real difference: 30.6 moves. Each key carried 2.001 forward pointers on average. A sorted array needs at most 17 binary search probes for 100,000 keys, but adding one shifts 50,000 entries on average.

A range costs its first search plus one read per key: finding where 100 keys begin averaged the same 31 moves. And the order of the keys, sorted or not, never makes a skip list lopsided the way sorted input does to a plain binary search tree.

The expensive edit is the cut. An audiobook with 1,000 chapters a minute apart, cut by 30 seconds near the start, had to move 998 chapters: 44,581 recorded steps, of them 38,583 moves, 2,035 unlinks, and 1,966 links, because each moved chapter got a new coin flip. A 10-second cut near the end moved one chapter in 100 steps. Adding one chapter, with both neighbor checks, took three searches and 48 moves. These are counts from the lesson’s TypeScript example, not timings.

07 / Give it a real job

Cut, and move the rest.

ChapterList writes the policy down. Starts are whole milliseconds; every chapter lasts at least 10 seconds, including the last one before the end; a description needs at least three chapters with the first at 00:00. A cut removes the chapters that start inside it and refuses when the chapter playing into it would end up shorter than 10 seconds.

Keying chapters by absolute start time makes lookups easy and cuts expensive, since every later key changes. An editor built around ripple edits might store each chapter’s length instead, so a cut changes one number, and pay for it when finding the chapter at a playhead, which then needs running totals. That is a different structure, chosen for a different workload.

Build UIs?See the day your planner’s day view has to say what is on now, what is next, and what fits on screen.

When you have to own it

You are building the day view of a planner. Keep the day’s events in an ordered map keyed by start minute, and the strip above the grid is two lookups: the floor of now is the event that started last, and the ceiling of the next minute is the one coming up. Neither walks the day one event at a time, however full it gets.

Then it gets real. The grid scrolls, so each scroll asks only for the events starting in the hours on screen: a range that begins at the floor of the top edge, because the event that started just above it can still be running into view. Dragging an event to 14:30 takes it out and puts it back under its new start. When another event already starts at 14:30, the planner puts it back where it was, says why, and keeps focus on it, so a keyboard user can try another time straight away.

Now and next in a day planner: the floor of the current minute is the event that started last, and the ceiling of the minute after it is the one coming up.

ReactAlready in your code
NowNext.tsx
import { useMemo } from 'react';
import type { SkipListMap } from '../chapters';
import { clock, type PlannerEvent } from './planner';

export function NowNext({
	events,
	version,
	now
}: {
	/** The day's events, keyed by start in minutes since midnight. */
	events: SkipListMap<PlannerEvent>;
	/** Changes whenever an event is added, moved, or removed, so the lookups run again. */
	version: number;
	/** The current minute, from 0 to 1439. */
	now: number;
}) {
	// Floor: the event that started last, at or before now. When events do not overlap, it is
	// the one in progress; overlapping events ask an interval tree instead.
	const current = useMemo(() => events.floor(now), [events, version, now]);
	// Ceiling of the next minute, so an event starting right now is not also next.
	const next = useMemo(() => events.ceiling(now + 1), [events, version, now]);
	const end = current ? current.key + current.value.minutes : 0;

	return (
		<p>
			<span>
				{current && now < end ? `Now: ${current.value.title} until ${clock(end)}` : 'Now: free'}
			</span>{' '}
			<span>{next ? `Next: ${next.value.title} at ${clock(next.key)}` : 'Nothing else today'}</span>
		</p>
	);
}

08 / Make the call

Ask whether the question is about neighbors.

Reach for an ordered map when you ask about keys near a value, not only at it: the latest before, the first after, everything between, and the keys keep changing while you ask.

Look elsewhere when that is not the question. For exact keys only, a hash map is simpler and faster. For a short or rarely changing list, a sorted array with binary search answers the same questions. If you need the guarantee rather than an expectation, a balanced tree gives it. If you only ever need the smallest key, a binary heap is enough. And if chapters could overlap, such as ad slots over a program, the question becomes which intervals contain a point, which an interval tree answers.

09 / Take the idea with you

Explain it without saying “ordered map.”

“I keep the chapter starts in time order on a chain, with some of them repeated on faster chains above, chosen by coin flips. To find what is playing, I run along the fastest chain while the next start is still earlier, drop to a slower one, and repeat. The last start I pass is the chapter playing, and the next one on the bottom chain is where Next chapter goes.” That describes the mechanism. The name is what you call it in a review.

Before moving on, explain three things without the name: why a hash map cannot say which chapter is playing, why Next chapter asks one millisecond later, and why a cut near the start touched every chapter. Then find a sorted array in your code that is searched with findLast or a loop, and decide whether binary search, or a map like this, would serve it better.

Connections to follow nextRelated lessons
  • Binary search tree answers the same questions with rotations instead of coin flips.
  • Binary search finds a floor or ceiling in a sorted array, which is all a short list needs.
  • Linked list is the bottom level of a skip list, and the reason reading along it is cheap.
  • Hash map is what Redis pairs with its skip list, for lookups by member.

Take the skip list into your editor. Insert 100,000 keys, count the moves to find each one, then change the coin to land heads a quarter of the time and count again.

Back to data structures & algorithms →