Algorithm Search⌘K

Proofs: Buffer tree

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

The buffer tree answers every query as of its time and keeps its invariants full

This proof shows: correctness, termination.

We use the pseudocode of the page, with m≥4m \ge 4, B≥1B \ge 1 and M=mBM = mB. The operations carry distinct time stamps 1,2,…1, 2, \ldots in the order of the input. For a time tt, let DtD_t be the set of the abstract dictionary after the operations with stamps at most tt. A query with stamp tt and key kk is correct if its answer is k∈Dt−1k \in D_{t-1}. The range of the root is all keys; the range of the child cic_i of xx is the part of the range of xx between the routing keys ri−1r_{i-1} and rir_i (with r0=−∞r_0 = -\infty and the last one +∞+\infty). For a node xx, let L(x)L(x) be the set of keys in the leaves below xx, and let P(x)P(x) be the sequence of operations in the buffers of xx and of all its descendants.

Claim. After each step of the algorithm, outside of a running call of EMPTY, these hold:

  • (T1) the routing keys of each node are increasing, the keys of each leaf are in its range, and all leaves are at one depth;
  • (T2) each operation in the buffer of xx has its key in the range of xx;
  • (T3) each buffer is in time order, and every operation in a buffer of yy is older than every operation in a buffer of a proper ancestor of yy;
  • (D) for every key kk, let tt be the stamp of the oldest operation on kk still in a buffer (or the current time plus one if there is none). Then k∈L(root)k \in L(\text{root}) if and only if k∈Dt−1k \in D_{t-1};
  • (Q) every answered query is correct.

Base. The empty tree has one bottom root with an empty leaf and an empty buffer. All five properties hold.

Lines L1 and L2. Line L1 appends a new operation, the youngest of all, to the root buffer. The root has no ancestor, the operation is younger than every buffered one, and the range of the root is all keys, so (T2) and (T3) hold. No leaf changes. For (D): if the new operation is on kk and no other operation on kk is buffered, then tt becomes the new stamp, and Dt−1D_{t-1} is the old current set, which holds kk exactly when LL does by (D) before the step. Otherwise tt does not change. Line L2 calls EMPTY, treated next.

EMPTY(x). We show by induction on the calls: if, when EMPTY(x)(x) starts, the buffers of all proper ancestors of xx are empty and (T1) to (T3), (D) and (Q) hold, then they hold when it returns.

Line L3 takes the buffer of xx, in time order by (T3). Consider an operation oo in it, with key kk.

Lines L5 and L6 (x not bottom). Line L5 routes oo to the unique child cc whose range holds kk, which exists by (T1) and (T2), so (T2) holds for cc. The operations are appended in time order, and each one is younger than every operation already in the buffer of cc or below it, by (T3) for xx before the step. The ancestors of cc other than xx have empty buffers, and xx is now empty, so (T3) holds. No leaf changes, and the set of buffered operations does not change, so (D) holds. Line L6 calls EMPTY on children of xx; the buffers of their ancestors, xx and above, are empty, so the induction hypothesis applies to each call, in turn. The calls of L6 on different children touch disjoint subtrees, except for splits, which we treat below.

Lines L7 to L10 (x bottom). Let kk be a key in the range of xx. By (T2) and (T3), every buffered operation on kk is in the buffer of xx or above, and the buffers above are empty, so all of them are in ops, and they are the youngest operations on kk there have been: every older one has already been applied. By (D), before line L8, k∈Sk \in S exactly when k∈Dt−1k \in D_{t-1}, where tt is the stamp of the first operation on kk in ops. Line L8 goes through ops in time order, and line L9 changes the membership of kk in SS exactly as the dictionary does: an insert adds kk, a delete removes it, and a query does not change it. So, by induction along the loop, when line L9 meets an operation with stamp ss on kk, k∈Sk \in S exactly when k∈Ds−1k \in D_{s-1}, because the operations with stamps between tt and ss that act on kk are exactly the earlier ones on kk in ops. So each query is answered correctly, which keeps (Q). After the loop, no operation on kk is buffered, and k∈Sk \in S exactly when k∈Dnowk \in D_{\text{now}}, which is (D). Line L10 writes SS in increasing order into leaves of at most BB keys and sets the routing keys to the first keys of the leaves after the first, so the routing keys are increasing and the keys of each leaf are in its range. The leaves are still one level below xx, so all leaves keep one depth: (T1) holds. Keys of other bottom nodes do not change.

SPLIT(x). Line L11 calls SPLIT only from a bottom node xx inside EMPTY(x)(x), after line L3, so the buffer of xx and of all its ancestors are empty. SPLIT goes up only to the parent, which is an ancestor of the first xx, so the same holds at every call. Line L12 cuts the c>mc > m children of xx into g=⌈c/m⌉g = \lceil c/m \rceil groups of sizes ⌊c/g⌋\lfloor c/g \rfloor or ⌈c/g⌉\lceil c/g \rceil, each at most mm and at least ⌊c/g⌋≥1\lfloor c/g \rfloor \ge 1. Line L13 gives each group its own node with the routing keys inside the group, and puts the routing keys between the groups into the parent, after the key that led to xx. So the routing keys of the parent stay increasing, and the range of each new node is the union of the ranges of its children, which keeps (T1). The new nodes are at the depth of xx, so the leaf depths do not change. Line L14 adds a new root above the old root and the new nodes when xx was the root; this adds one to the depth of all leaves at once, which keeps them equal. All moved nodes have empty buffers, and no operation moves, so (T2), (T3), (D) and (Q) hold. Line L15 repeats the step at the parent, which can now have more than mm children, and stops at a node with at most mm children, so after SPLIT every node has at most mm children.

A split can move a sibling c′c' of the current path, whose buffer is not empty, to a new node. This is harmless: c′c' keeps its subtree and its range, and its new parent has an empty buffer, so (T2) and (T3) hold for its operations. When the loop of line L6 then calls EMPTY on c′c', its ancestors are still empty, since only the path of the current call can have held operations and all of it was emptied.

Buffer sizes. After an operation, no buffer is full: line L2 empties the root when it overflows, and line L6 empties each child that overflows after line L5. A child that is not full when line L6 runs is not touched again in that call, so at the end every buffer holds at most MM operations.

Flush. The flush calls EMPTY top-down on every node with a nonempty buffer, starting at the root. In each call the buffers of the ancestors are empty, since the parent was emptied first in the same pass, so the arguments above apply. The pass repeats while a buffer is nonempty. When it ends, no operation is buffered, so by (Q) every query has been answered, and by (D) the leaves hold exactly the current set DnowD_{\text{now}}. The global rebuild flushes first and then builds a tree with empty buffers from the sorted keys, with groups of at most mm children and all leaves at one depth, so it keeps all five properties.

Termination. Each call of EMPTY at a non-bottom node moves every operation in its buffer one level down, and EMPTY-BOTTOM removes the operations it gets. An operation is in exactly one buffer, and the tree has finite height, so the number of moves of each operation is bounded by the height plus one, and the recursion of EMPTY ends. SPLIT goes up one level per call and stops at the root. Each pass of the flush either empties all buffers or moves some operation down, because it calls EMPTY on the root and on every nonempty buffer it reaches; operations only move down and new operations do not arrive during a flush, so the flush ends after finitely many passes.

So every query gets its correct answer by the end of the flush at the latest, and the leaves always hold the current set once all buffers are empty. ■\blacksquare

Back to Buffer tree