Algorithm Search⌘K

Proofs: B^ε-tree

These are the human proofs of B^ε-tree, at full length.

Searches in the B^ε-tree return the newest value and updates keep the invariants full

This proof shows: correctness, termination.

We use the pseudocode of the page, with F≥3F \ge 3, C≥2C \ge 2 and L≥2L \ge 2. The range of the root is all keys, and the range of the child cic_i of xx is the part of the range of xx between the pivots pi−1p_{i-1} and pip_i, closed on the left and open on the right, with p0=−∞p_0 = -\infty and pf=+∞p_f = +\infty. For a key kk and a node xx whose range holds kk, define the state of kk at xx:

  • if xx is a leaf, kk is present with value vv when the pair (k,v)(k, v) is in xx, and absent otherwise;
  • if xx is internal and x.bufx.buf has a message on kk, the state is that of the message: present with vv for PUT(v), absent for DEL;
  • otherwise, the state of kk at the child of xx whose range holds kk.

Let σ(k)\sigma(k) be the state of kk after all updates so far, in the abstract dictionary. We prove that between operations:

  • (E1) the pivots of each node are increasing and inside its range, all leaves are at one depth, each internal node has between 2 and FF children, and each leaf has at most LL keys, all in its range;
  • (E2) each message in x.bufx.buf is on a key in the range of xx;
  • (E4) every buffer has at most CC messages;
  • (S) for every key kk, the state of kk at the root is σ(k)\sigma(k).

Property (E3) of the page is what makes (S) survive a flush; we use it in the form of the definition of state, which looks at the highest message first.

Base. The tree is one empty leaf, and every key is absent in the dictionary and at the root.

GET. Lines L1 to L5 follow the definition of state: at each internal node, line L3 returns the state of the message on kk if there is one, and line L4 goes to the child whose range holds kk, which exists and is unique by (E1). At the leaf, line L5 returns the state there. So GET returns the state of kk at the root, which is σ(k)\sigma(k) by (S). It ends because the height is finite, and it changes nothing.

UPSERT, line L6. If the root is a leaf, applying the message makes kk present with vv or absent, which is the new σ(k)\sigma(k); other keys keep their state. The leaf stays sorted. It may have L+1L + 1 keys, which line L8 handles.

UPSERT, line L7. The message goes into the root buffer and replaces any older message on kk. Now the state of kk at the root is that of the new message, which is the new σ(k)\sigma(k). Other keys are not affected. Since the range of the root is all keys, (E2) holds. The buffer may have C+1C + 1 messages, and FLUSH is called.

FLUSH(x). We show: if, when FLUSH(x)(x) is called, (E1) and (E2) hold except that x.bufx.buf may have more than CC messages and the states at xx of all keys are given, then when it returns, x.bufx.buf has at most CC messages, every buffer below xx has at most CC, every child of xx satisfies (E1) but xx itself may have more than FF children, and the state of every key at xx is unchanged.

Lines L10 and L11. Let cc be the chosen child and GG the set of messages on keys in the range of cc, all moved to cc. For a key kk with a message in GG: before the move, its state at xx was given by that message. After the move, xx has no message on kk, so the state at xx is the state at cc. If cc is a leaf, line L11 applied the message to kk in the leaf, so the state at cc is that of the message. If cc is internal, the message is now in c.bufc.buf, having replaced any older message on kk there, so again the state at cc is that of the message. For a key with no message in GG, nothing on its path changed. So the state of every key at xx is unchanged. The moved messages are on keys in the range of cc, so (E2) holds for cc.

Line L12. If c.bufc.buf now has more than CC messages, the recursive call FLUSH(c)(c) keeps the states at cc, so also at xx, and returns with the buffers of cc and below at most CC.

Line L13. If cc has more than LL keys or more than FF children, SPLIT cuts it.

SPLIT (line L14). Let cc be a leaf with keys k1<⋯<ksk_1 < \cdots < k_s, s>Ls > L, and h=⌊s/2⌋≥1h = \lfloor s/2 \rfloor \ge 1. The left half keeps k1,…,khk_1, \ldots, k_h, the right half gets kh+1,…,ksk_{h+1}, \ldots, k_s, and kh+1k_{h+1} becomes a pivot of the parent, between the pivots around cc. Since k1<kh+1k_1 < k_{h+1} and all keys of cc are in its range, the new pivot is strictly inside the range of cc, so the pivots of the parent stay increasing, and each key is in the range of its new leaf. If cc is internal with f>Ff > F children, h=⌊f/2⌋≥2h = \lfloor f/2 \rfloor \ge 2, the left half keeps the children 1..h1..h and their pivots, the right half the others, and the pivot php_h between them goes up. Each message of c.bufc.buf goes to the half whose range holds its key. Then each key has, in the half that holds it, the same message and the same subtree as before, so its state is unchanged. Each half has between 2 and FF children if f≤2Ff \le 2F, and the repeat in line L14 splits halves that are still too big, which happens a finite number of times since each split makes the halves smaller. The new nodes are at the depth of cc, so all leaves stay at one depth. The parent gains children, which is allowed during FLUSH.

Line L9. The loop ends: each pass moves a nonempty group of messages out of x.bufx.buf, since ∣x.buf∣>C≥1|x.buf| > C \ge 1, and nothing is added to x.bufx.buf during the loop. When it ends, ∣x.buf∣≤C|x.buf| \le C.

UPSERT, line L8. After line L6 or L7, only the root may be too big: every other node satisfies (E1) by the claim on FLUSH. While the root is too big, line L8 puts a new root with one child above it and splits that child, as in line L14. The new root has an empty buffer and its range is all keys, so all states are kept; the depth of all leaves grows by one at once; and after the split, every child of the new root satisfies (E1), because SPLIT repeats on each half that is still too big. The new root has at least 2 children. If it has more than FF, the loop runs again with it. Each round adds a level above nodes of at most FF children and replaces a root of ff children by one of about f/Ff/F or fewer, so the loop ends, and then (E1) holds.

Deletes and the rebuild. A delete is an update with the message DEL, covered above. The rebuild computes, for each key, its state at the root, by resolving each buffer over the content of its subtree from the bottom up, which is the definition of state; it then builds a tree with empty buffers from the present keys, with leaves of at most LL keys and nodes of 2 to FF children. So (S) and (E1) to (E4) hold after it.

Termination. GET follows one path. FLUSH(x)(x) recurses only into children of xx, so the depth of the recursion is at most the height, and each loop ends as shown. SPLIT recurses only on smaller nodes. So every operation ends.

Hence every GET returns the value of the newest update of its key, or "absent", and all invariants hold between operations. ■\blacksquare

Back to B^ε-tree