Algorithm Search⌘K

Proofs: Adaptive radix tree

These are the human proofs of Adaptive radix tree, at full length.

The adaptive radix tree operations keep the invariant and implement the symbol table full

This proof shows: correctness.

We use the representation, the invariant (A1) to (A3) and the pseudocode of the page. All keys, stored or queried, end with the terminator byte 0 and contain no other 0, so no key is a proper prefix of another. For a node xx, path(x)path(x) is the string of xx: the concatenation, from the root down to the parent of xx, of each inner node's prefix and the byte of the edge taken. Write depth(x)=∣path(x)∣depth(x) = \lvert path(x) \rvert. By (A1), every leaf below xx has a key that starts with path(x)path(x), and if xx is inner, with path(x)⋅x.prefixpath(x) \cdot x.prefix.

Lemma 1 (layouts). FIND-CHILD(xx, bb) returns the child of xx with byte bb or NIL; ADD-CHILD, REMOVE and the change of layout keep the set of (byte, child) pairs, except for the one pair added or removed.

Proof. In Node4 and Node16, the bytes and the children are parallel arrays, so the child of bb is at the index of bb. In Node48, the index array maps bb to the slot of its child or to −1-1. In Node256, the slot at bb holds the child or NIL. A change of layout reads all pairs of the old node and adds them to the new one. □\square

Lemma 2 (search). Under (A1) to (A3), the walk of lines L1 to L4 on a key kk ends at the unique leaf whose key is kk if kk is stored, and otherwise at NIL or at some leaf.

Proof. Suppose kk is stored, at leaf λ\lambda. We show that the walk keeps the claim "xx is an ancestor of λ\lambda (or λ\lambda itself) and d=depth(x)d = depth(x)". This holds at the root with d=0d = 0. At an inner ancestor xx of λ\lambda, (A1) gives that kk starts with path(x)⋅x.prefixpath(x) \cdot x.prefix, so line L2 passes and line L3 sets d=depth(x)+∣x.prefix∣d = depth(x) + \lvert x.prefix \rvert. The child of xx on the way to λ\lambda has byte k[d]k[d], because kk starts with the path of that child, and by Lemma 1 and the distinct bytes of (A2), FIND-CHILD returns it. Line L4 then sets dd to the depth of the child. The walk ends at a leaf, which must be λ\lambda. Uniqueness follows from (A3) and the fact that a map stores each key once (as shown for PUT below). If kk is not stored, any leaf reached has a different key. □\square

GET. By Lemma 2, line L5 returns the value of kk exactly when kk is stored, because it compares the full key at the leaf.

PUT. If kk is stored, the walk reaches its leaf exactly as in Lemma 2, line L8 finds it and replaces the value, and the tree does not change otherwise. Assume now that kk is not stored. The walk of lines L8 to L17 keeps the claim of Lemma 2 for the prefix of kk it has matched so far: d=depth(x)d = depth(x), and kk starts with path(x)path(x). It ends in one of three ways.

  • Lines L9 and L10, at a leaf xx. Its key x.keyx.key starts with path(x)path(x), as does kk, and x.key≠kx.key \ne k. Neither is a prefix of the other, by the terminator, so they first differ at some position d+pd + p with p≥0p \ge 0, and the bytes x.key[d+p]x.key[d + p] and k[d+p]k[d + p] exist and differ. The new Node4 sits where xx was, so its path is path(x)path(x); its prefix k[d..d+p−1]k[d .. d+p-1] is shared by both keys; its two children have distinct bytes. So (A1) holds for both leaves, and the new node has 2 children, so (A2) holds.
  • Line L12, inside the prefix of an inner node xx. The keys below xx start with path(x)⋅x.prefixpath(x) \cdot x.prefix, and kk agrees with x.prefixx.prefix on its first mm bytes and then differs. The new Node4 takes the place of xx with prefix x.prefix[0..m−1]x.prefix[0 .. m-1]; xx hangs below it at the byte x.prefix[m]x.prefix[m] with prefix x.prefix[m+1..]x.prefix[m+1 ..], so the path string of every node below xx, through the new node, the byte and the shortened prefix, is unchanged; and the new leaf hangs at the byte k[d+m]≠x.prefix[m]k[d + m] \ne x.prefix[m]. So (A1) and (A2) hold.
  • Lines L14 to L16, at an inner node with no child for k[d]k[d]. If xx is full, its pairs are copied into the next larger layout (Lemma 1), which has room. The new leaf gets the byte k[d]k[d], which no other child has, and its path is path(x)⋅x.prefix⋅k[d]path(x) \cdot x.prefix \cdot k[d], a prefix of kk. So (A1) holds, and the count grows by one, staying in the range of the layout.

In each case one leaf is added, so (A3) holds with the new key counted. Line L17 moves to a child whose path is a prefix of kk, so the claim holds there. The walk ends because dd grows and kk is finite.

DELETE. If kk is not stored, line L19 returns without change, by Lemma 2. Otherwise the walk ends at the leaf of kk, and line L20 removes it from its parent qq, which removes exactly one key, so (A3) holds, and (A1) is unaffected for the other leaves. Now qq may break (A2).

  • If qq has one child cc left (line L21), qq is replaced by cc. If cc is a leaf, its path becomes path(q)path(q), which is a prefix of its old path, so (A1) still holds. If cc is inner, its new prefix is q.prefix⋅b⋅c.prefixq.prefix \cdot b \cdot c.prefix, where bb is its byte in qq, so path(c)⋅c.prefixpath(c) \cdot c.prefix is unchanged, and so are the paths of all nodes below cc. The parent of qq keeps the same number of children.
  • If qq reaches its shrink threshold (line L22), its pairs move to the smaller layout, which has room for them, and by the choice of thresholds the count is in the range of the new layout.
  • Otherwise qq keeps at least the lower end of its range, by the choice of thresholds, since its count fell by one from within its range and did not reach the threshold.

So (A1) to (A3) hold after the deletion.

Order. The walk over all leaves visits the children of each inner node in increasing byte order, by Lemma 1. Two keys in different subtrees of a node xx agree on path(x)⋅x.prefixpath(x) \cdot x.prefix and differ at the next byte, which is the byte of their edges, so the leaves come out in increasing byte order of their keys. Removing the terminator does not change the order, because it is the smallest byte and is the last byte of each key. ■\blacksquare

Back to Adaptive radix tree