Structures

The priority nobody supplied

Insert 4,096 sorted keys into a binary search tree and it reaches height 4,095, costing 8,386,560 comparisons to build. Give every key a second, random key and keep the tree heap-ordered on that instead, and the same insertion reaches height 26 for 32,750 comparisons. Nothing detected the imbalance, and nothing rebalanced.

The tree that is a list measured the binary search tree’s failure and did not fix it. The failure is worth restating in one line because everything here is a response to it: insert keys in sorted order and every node gets one child, so the structure whose reputation is O(logn)O(\log n) performs a linear scan, on the most ordinary arrival order there is.

The usual repair is to balance. Detect that one subtree is taller than the other, rotate to correct it, and prove that the maintained invariant implies a height bound. AVL trees measure heights; red-black trees maintain a colour invariant; both are correct, both are proved, and both require getting a case analysis right that most people who can describe the idea cannot write from memory.

A treap does not do any of that. It gives every key a second key, drawn at random, and keeps the tree ordered by the first and heap-ordered by the second. There is no imbalance detection anywhere in it.

A treap of 24 keys, sorted insertion, seed 20260810Horizontal position is the key; vertical position is the random priority the key was given on arrival, highest at the top. The search-tree property is that no edge crosses another; the heap property is that no edge goes upward. This tree is 8 deep against an ideal of 4, it took 20 rotations to build, and the insertion order was sorted — which for an ordinary binary search tree would be the difference between a tree and a list.root, priority 0.9651.00.0prioritykey24 keys, sorted insertion, seed 20260810height 8 against an ideal of 4
Fig. 1 Twenty-four keys inserted in sorted order. Horizontal position is the key; vertical position is the random priority it arrived with, highest at the top. The search-tree property is that no edge crosses another; the heap property is that no edge goes upward. This tree is eight deep against an ideal of four, and it took twenty rotations to build from an input that would leave an ordinary binary search tree twenty-three deep.

Two orders at once

A treap is a binary search tree in the keys and a max-heap in the priorities, simultaneously. Those two requirements sound like they might conflict and they do not: for any set of (key, priority) pairs with distinct priorities there is exactly one tree satisfying both, and it is easy to describe. The root is the pair with the highest priority. Everything with a smaller key goes left, everything with a larger key goes right, and each side is built the same way recursively.

The uniqueness is the whole trick and it is worth pausing on, because it is what makes the insertion order stop mattering. The final shape is a function of the set of pairs. It does not depend on the order they arrived in, because the description above never mentions an order.

Insertion is short. Walk down as in an ordinary binary search tree and attach the new node as a leaf, which satisfies the key order and probably violates the heap order. Then rotate the new node upward while its priority exceeds its parent’s. A rotation preserves the key order — that is what a rotation is for — so the loop is repairing one property without breaking the other, and it stops as soon as the heap order holds.

There is no case analysis. There is one loop with one condition in it.

What sorted insertion does to each

The measurement is the same input on both structures, at four sizes.

nn BST height BST comparisons treap height treap comparisons treap rotations
64 63 2,016 11 188 59
256 255 32,640 14 1,195 251
1,024 1,023 523,776 21 6,526 1,020
4,096 4,095 8,386,560 26 32,750 4,088

The binary search tree’s height is exactly n1n - 1 at every size, which is not an approximation — sorted insertion produces a right spine and nothing else, so the height is the number of keys after the first. The comparison count is exactly (n2)\binom{n}{2} for the same reason.

The treap’s height at n=4,096n = 4{,}096 is 26, against log2n=12\lfloor \log_2 n \rfloor = 12. That factor of just over two is the expected behaviour: the expected height of a treap on nn random priorities is about 4.31lnn4.31\ln n, which at 4,096 is 35.8, and 26 is a comfortable build. The comparison count is 32,750, or eight per key.

Tree height against n, sorted insertionHeight on logarithmic axes for the same keys inserted into an ordinary binary search tree and into a treap. At n = 4,096 the plain tree is 4095 deep and the treap is 26, against an ideal of 12. The treap does no balancing and detects nothing: each key arrives with a random priority, the tree is kept heap-ordered on those priorities, and the shape that results is the shape a random insertion order would have given — whatever order the keys actually came in.10010³1010010³keys insertedheightbinary search treetreap⌊log₂ n⌋sorted insertion, keys from seed 909, priorities from 910158× at n = 4,096
Fig. 2 Height against n on logarithmic axes, for the same sorted keys inserted into both structures. The plain tree’s line is the diagonal — height equals n − 1 exactly, which on these axes is a straight line of slope one. The treap’s line is nearly flat by comparison, sitting between two and three times ⌊log₂ n⌋ over the whole range.

The rotations are the whole of the work, and there are n of them

The number of rotations is the quantity that says what a treap costs relative to a plain tree, and the table above has an unexpected regularity in it: 59 rotations for 64 keys, 251 for 256, 1,020 for 1,024, 4,088 for 4,096. That is almost exactly one rotation per key.

On sorted insertion that is easy to see. Every arriving key is the largest so far, so it attaches as the rightmost leaf and then climbs until it meets a higher priority. The expected number of steps in that climb is bounded by a constant — the arriving priority has to beat each ancestor in turn, and beating jj in a row has probability 1/(j+1)!1/(j+1)!-ish — so the total is Θ(n)\Theta(n) and the constant is under one.

This is the honest comparison against a red-black tree, which also does O(1)O(1) amortised rotations per insertion. The treap does not win on rotation count. It wins on there being one loop rather than a case analysis, and it pays for that with the second key.

The shape is the shape a random order would have given

The sentence that makes a treap more than a trick is this one: the tree a treap builds from any insertion order is distributed exactly as an ordinary binary search tree built from a uniformly random insertion order.

The argument is one line, given the uniqueness above. The final shape depends only on the set of (key, priority) pairs. The pair with the highest priority is the root. If the priorities are independent and uniform, the highest one is equally likely to belong to any of the nn keys — so the root is a uniformly random key, and the same holds recursively in each subtree. That is precisely the process that generates a random binary search tree.

So everything known about random binary search trees transfers, including the constant: expected depth 2lnn1.39log2n2\ln n \approx 1.39\log_2 n, expected height 4.31lnn3.0log2n4.31\ln n \approx 3.0 \log_2 n. And the measurement can check the transfer directly, by building an ordinary BST from random keys and a treap from sorted keys and comparing:

n=4,096n = 4{,}096 height mean node depth
plain BST, random insertion 28 14.65
treap, sorted insertion 26 13.06

The treap on the worst possible input is slightly shallower than the plain tree on a good one. Those two numbers are draws from the same distribution and the difference between them is noise, which is exactly the claim.

A treap of 24 keys, shuffled insertion, seed 20260810Horizontal position is the key; vertical position is the random priority the key was given on arrival, highest at the top. The search-tree property is that no edge crosses another; the heap property is that no edge goes upward. This tree is 6 deep against an ideal of 4, it took 40 rotations to build, and the insertion order was shuffled — which for an ordinary binary search tree would be the difference between a tree and a list.root, priority 0.9651.00.0prioritykey24 keys, shuffled insertion, seed 20260810height 6 against an ideal of 4
Fig. 3 The same twenty-four keys shuffled before insertion, same seed. A different tree — different priorities met different keys — and no better or worse than the sorted one in any systematic way. This is what “the insertion order does not matter” looks like when it is true of the resulting shape rather than merely of the asymptotic class.

Why not simply shuffle the input

If the shape a treap produces is the shape a random insertion order would have produced, an obvious question is why not shuffle the keys and use an ordinary tree. It is a fair question and the answer is the reason treaps exist rather than being a curiosity.

Shuffling requires having all the keys before inserting any of them. A tree that is built once from a known set can indeed be shuffled, and for that case the plain tree with a shuffle is simpler and cheaper. But the interesting use of a search tree is one that is inserted into and deleted from over its lifetime, by a program that does not know what is coming. There is no batch to shuffle. The keys arrive when they arrive, frequently in an order correlated with something — timestamps, identifiers, sorted output from an earlier stage — and that correlation is exactly the failure mode.

A treap needs no batch. It randomises at the point of insertion, one key at a time, and the guarantee holds after every operation rather than only at the end of a build. That is the same distinction as between an average case and an expected case, applied to when the randomness is available.

Deletion makes the point sharper still. Deleting from an ordinary binary search tree by the standard two-child rule — promote the in-order successor — is known to skew the tree over long sequences of interleaved insertions and deletions, and no amount of shuffling the original input helps, because the damage accumulates afterwards. A treap deletes by rotating the doomed node down until it is a leaf and cutting it off, which leaves the remaining set of (key, priority) pairs untouched — and the shape of a treap is a function of that set alone. So the tree after a deletion is the tree that set would have produced from scratch. The invariant survives the operation rather than degrading under it.

Across builds, because one build is an anecdote

Everything above is one seed. A treap’s height is a random variable in the same way a skip list’s is, so the useful statement is about the distribution.

Two hundred treaps over the same 2,048 sorted keys, one seed each:

  • Height ranged from 20 to 29, mean 23.89, against log22048=11\log_2 2048 = 11 and the theoretical 4.31lnn32.84.31\ln n \approx 32.8.
  • Mean node depth — the quantity a lookup actually pays — had a relative standard deviation of 5.29%, around a mean of 12.48.

The pattern is the one the previous essay established. The extreme — the height — varies by 45% across builds. The average — what a lookup costs — varies by 5%. The structure is volatile in the quantity that is cheap and steady in the quantity that is paid for, and that is the property that makes an expected-case bound worth having.

The same silhouette, three more draws

The shape is the priorities and the priorities are drawn, so the honest way to show that is to draw them again.

A treap of 24 keys, sorted insertion, seed 424242Horizontal position is the key; vertical position is the random priority the key was given on arrival, highest at the top. The search-tree property is that no edge crosses another; the heap property is that no edge goes upward. This tree is 6 deep against an ideal of 4, it took 17 rotations to build, and the insertion order was sorted — which for an ordinary binary search tree would be the difference between a tree and a list.root, priority 0.9381.00.0prioritykey24 keys, sorted insertion, seed 424242height 6 against an ideal of 4
Fig. 4 The same twenty-four keys inserted in the same sorted order under a different seed. A different tree, the same kind of tree, and nothing about the keys decided either.
A treap of 36 keys, sorted insertion, seed 999Horizontal position is the key; vertical position is the random priority the key was given on arrival, highest at the top. The search-tree property is that no edge crosses another; the heap property is that no edge goes upward. This tree is 10 deep against an ideal of 5, it took 34 rotations to build, and the insertion order was sorted — which for an ordinary binary search tree would be the difference between a tree and a list.root, priority 0.9791.00.0prioritykey36 keys, sorted insertion, seed 999height 10 against an ideal of 5
Fig. 5 Thirty-six keys under a third seed. The height creeps up as the logarithm does and the silhouette stays the silhouette of a random binary search tree, which is the whole of what the priorities buy.

And the last of the three changes the size rather than the draw, which is the direction the logarithm is supposed to show up in.

A treap of 48 keys, sorted insertion, seed 20260810Horizontal position is the key; vertical position is the random priority the key was given on arrival, highest at the top. The search-tree property is that no edge crosses another; the heap property is that no edge goes upward. This tree is 11 deep against an ideal of 5, it took 45 rotations to build, and the insertion order was sorted — which for an ordinary binary search tree would be the difference between a tree and a list.root, priority 0.9921.00.0prioritykey48 keys, sorted insertion, seed 20260810height 11 against an ideal of 5
Fig. 6 And forty-eight under the original seed. Twice the keys of the opening plate, one or two levels deeper, and still no relationship at all between where a key sits and what a key is.

The failure this essay nearly shipped

Every other input kind the site keeps behaves the way the argument predicts, and one of them did not, for a reason that turned out to be the most useful thing measured here.

At n=4,096n = 4{,}096, treap heights on the site’s five input kinds: sorted 26, reversed 26, nearly sorted 26, few distinct values 78, random 3,849.

Three thousand eight hundred and forty-nine, on a structure whose whole claim is that no input can do that — and on the easy input, the one where an ordinary binary search tree measures 28 and needs no help at all. The treap was 137 times worse than the thing it replaces, on the input it was not needed for.

The cause is not in the treap. The site’s random input comes from INPUTS.random(n, 909), which reads a seeded xorshift32 stream; the treap’s priorities came from Coins(909), which reads the same stream from the same starting state. So the ii-th key and the ii-th priority were the same thirty-two bits scaled two different ways. Printed side by side:

key / 16384 priority
0.055725 0.055753
0.156250 0.156292
0.494873 0.494893
0.630554 0.630596

The priority was the key. And a treap whose priorities are monotone in its keys is not a treap in any useful sense: the heap order and the search order now agree, so the highest priority is the largest key, which becomes the root with nothing to its right, and the structure is a spine.

Two things about this are worth carrying away.

The independence of the keys from the coins is an assumption, and it is one a program has to arrange. Every analysis in this essay says “priorities drawn independently at random”. Nothing in the code enforces it, nothing in the type system suggests it, and the natural thing to do in a test — seed everything from one number so the run reproduces — breaks it exactly. The figures now draw their keys from one seed and their priorities from another, and both are printed in the caption strip for that reason.

The failure is silent. The tree was still a correct search tree. Every lookup returned the right answer, every key came out of an in-order walk in order, and no assertion about correctness would have fired. The only visible symptom was a number in a figure, and the only reason it was seen is that the figure’s own assertion demanded the height stay under 5log2n5\log_2 n and refused to draw when it did not. That is the argument for figures that check themselves in one sentence.

The remaining input is worth a line too. Few distinct values gives a treap of height 78 against 26 on the others, because 4,096 keys drawn from eight values means long runs of equal keys, and equal keys all go to the same side. That is not a randomisation failure — the plain tree measures 525 on the same input — but it is a reminder that a treap’s guarantee is about distinct keys, and duplicates degrade it in the same way they degrade the floor when the values repeat.

Tree height against n, random insertionHeight on logarithmic axes for the same keys inserted into an ordinary binary search tree and into a treap. At n = 4,096 the plain tree is 28 deep and the treap is 29, against an ideal of 12. The treap does no balancing and detects nothing: each key arrives with a random priority, the tree is kept heap-ordered on those priorities, and the shape that results is the shape a random insertion order would have given — whatever order the keys actually came in.10010³10keys insertedheightbinary search treetreap⌊log₂ n⌋random insertion, keys from seed 909, priorities from 202608101× at n = 4,096
Fig. 7 The same comparison on random insertion, with the priorities drawn from a different stream than the keys. Here the plain tree is fine — random insertion is the good case for it — and the two lines sit close together, both a small multiple of log₂ n. This is the figure that says what a treap buys: nothing at all, on the input a binary search tree already handles. What it buys is that this picture and the sorted one look the same.

What the repair costs

Three costs, all measurable, and worth listing because “randomisation makes the problem go away” is not free.

Space. A priority per node. Thirty-two bits if it is a full word, which on a tree of small keys can be a meaningful fraction of the node — an integer key with two child pointers is 20 bytes, and a 4-byte priority makes it 24. A red-black tree needs one bit, which usually fits in the low bit of a pointer and costs nothing. That is the clearest place a treap loses.

Randomness. Thirty-two bits per key drawn from the generator, measured, fitted against nn with a spread of 1.000 — because it is exactly 32 per key with no variance at all. A skip list needs two. Both are Θ(n)\Theta(n) and the constant differs by a factor of sixteen, which is the sort of thing the notation drops and the randomness counter does not.

The guarantee. A red-black tree’s height is at most 2log2(n+1)2\log_2(n+1), proved, for every input. A treap’s height is a random variable whose mean is about 3log2n3\log_2 n and which has no bound. On the measurement above it never exceeded 29 in two hundred builds at n=2,048n = 2{,}048, where a red-black tree is guaranteed under 22. The treap is usually worse than the red-black guarantee and never catastrophically worse, and if the application has a hard latency ceiling that difference is the one that decides.

What it buys is the thing the table at the top of this essay measures: a structure that survives sorted insertion, built from one loop, with no invariant to state and no configuration to get wrong.

Thirty-two bits is a choice, and it is not a constant

Two of the three costs above are quoted as thirty-two bits a key, and the number is worth interrogating, because the treap does not use a priority’s value at all — only its rank against the other priorities. What the width has to buy is distinctness, and distinctness has a size.

The uniqueness argument this whole essay rests on requires the priorities to be distinct: two equal priorities and the shape stops being a function of the set. With nn keys and bb-bit priorities the expected number of collisions is about n2/2b+1n^2/2^{b+1}, so keeping it under one needs

b>2log2n.b > 2\log_2 n.

At the sizes measured here that is comfortable: 4,096 keys need 24 bits, and thirty-two gives an expected 0.002 collisions. At 65,536 keys the expectation is 0.5, and at a million it is 116 — so a thirty-two-bit priority is under-provisioned past about sixty-five thousand keys, which is not a large tree.

The failure is gentle rather than dramatic, which is why nobody notices it. A hundred tied pairs in a million-key treap perturb a hundred local shapes and leave the height essentially where it was; the structure stays a correct search tree and its depths stay where the distribution says. What breaks is the statement — the shape is no longer a function of the set alone, so the argument that a deletion returns the tree that set would have built stops being exactly true. A quiet weakening of a proof rather than of a program.

The more interesting consequence is what it does to the randomness accounting. Thirty-two bits a key against a skip list’s two is quoted as a factor of sixteen and filed as a constant the notation drops. It is not a constant. A skip list’s two bits are two bits at every nn — a geometric level draw has mean two flips whatever the structure holds — while a treap’s width has to track 2log2n2\log_2 n to keep its own premise true.

So the honest comparison is Θ(nlogn)\Theta(n\log n) bits against Θ(n)\Theta(n), and the ratio grows: twelve at four thousand keys, twenty at a million, twenty-four at a billion. The factor of sixteen is what one fixed word width looks like at one size, and reading it as the exchange rate between the two structures is exactly the error counting the coin flips exists to catch — a counter reporting a per-key figure that is really a per-key-times-something figure with the something held still.

Which leaves the practical guidance in a better place than thirty-two bits. Size the priority at 2log2n2\log_2 n plus a margin, from the largest nn the tree is expected to reach: twenty bits for a thousand keys, forty for a million. Below sixty-five thousand keys a thirty-two-bit word is generous and could be trimmed to a sixteen-bit field at a few thousand keys, which halves the space cost this essay names as the treap’s clearest loss against a red-black tree. Above it, thirty-two is the wrong number in the direction nothing reports, and the height is a distribution’s warning applies to the priorities as much as to the shape they produce: a quantity that only matters through its extremes is a quantity whose mean says nothing about whether it is large enough.

The one it does not fix

A treap removes the input’s ability to choose the tree’s shape. It does not remove an adversary’s, and the distinction is the same one that runs through this whole phase.

The priorities come from a seeded generator. If the seed is known, the priorities are known, and the tree that any sequence of keys will produce is computable in advance — so an adversary can order the insertions to produce whatever shape they like, including the spine. That is not a hypothetical about treaps in particular; it is the general fact that a published seed is a published function, and it applies to every structure in this field.

What survives is the claim about inputs, which is the claim that was made: for keys arriving in any order chosen without reference to the priorities — that is, for the data an ordinary program encounters — the treap’s shape is drawn from the random-insertion distribution, and the arrival order has no effect on it whatever. Sorted, reversed, nearly sorted, adversarial against a different seed: all the same. That is a strictly stronger statement than “a BST is fine on random data”, it is the difference between an average-case and an expected-case guarantee, and it is bought with thirty-two bits per node and one loop.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Binary searchBinary search treeDistributionGuaranteeHeapHeap orderInsertion orderRandomised binary search treeRotationSkip listTreap