Algorithm Search⌘K

Proofs: Ternary search tree

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

The ternary search tree operations keep the invariant and answer the string symbol table queries full

This proof shows: correctness.

We use the pseudocode, the sibling trees, the strings str(x)str(x) and the invariant (S1) to (S4) of the page. The level of a sibling tree is the length of the strings of its nodes: the sibling tree of the root has level 1, and the one below y.midy.mid has level ∣str(y)∣+1\lvert str(y) \rvert + 1. All nodes of one sibling tree share the same strstr except for the last symbol.

Lemma 1 (uniqueness). Under (S1), each nonempty string ss is str(x)str(x) for at most one node xx.

Proof. By induction on ∣s∣\lvert s \rvert. A node with str(x)=sstr(x) = s lies in a sibling tree of level ∣s∣\lvert s \rvert and has x.c=s[∣s∣−1]x.c = s[\lvert s \rvert - 1]. For ∣s∣=1\lvert s \rvert = 1 that sibling tree is the one of the root. For ∣s∣>1\lvert s \rvert > 1 it is the sibling tree below y.midy.mid for a node yy with str(y)=s[0…∣s∣−2]str(y) = s[0 \ldots \lvert s \rvert - 2], and yy is unique by the induction hypothesis. In either case the sibling tree is fixed, and by (S1) its symbols are distinct, so at most one of its nodes has the symbol x.cx.c. □\square

Lemma 2 (walk). The loop of lines L1 to L7 returns x.valx.val for the node xx with str(x)=sstr(x) = s if this node exists, and NIL otherwise.

Proof. The loop keeps this claim: xx is NIL or a node of the sibling tree of level d+1d + 1 whose nodes have strings s[0…d−1]⋅cs[0 \ldots d-1] \cdot c, and if a node zz with str(z)=s[0…d]str(z) = s[0 \ldots d] exists, then zz is in the subtree of xx within that sibling tree. At the start, xx is the root and d=0d = 0, so the claim holds. Lines L3 and L4 are one step of a BST search for the symbol s[d]s[d]: by (S1) a node with symbol s[d]s[d] smaller than x.cx.c can only be in the left subtree, and one with a larger symbol only in the right subtree. If s[d]=x.cs[d] = x.c, then str(x)=s[0…d]str(x) = s[0 \ldots d]. If d=∣s∣−1d = \lvert s \rvert - 1, then xx is the node of ss, and line L6 returns its value. Else line L5 moves to the sibling tree below x.midx.mid, which by definition holds exactly the nodes with strings s[0…d]⋅cs[0 \ldots d] \cdot c, and the claim holds for d+1d + 1. If xx becomes NIL, the subtree where zz must be is empty, so the node zz of s[0…d]s[0 \ldots d] does not exist, and then no node of ss exists either (its sibling tree would be below z.midz.mid). Line L7 returns NIL. Each round either moves one level down in a sibling tree or increases dd; a sibling tree is finite and d≤∣s∣−1d \le \lvert s \rvert - 1, so the loop ends. □\square

GET. By Lemma 2 and (S2), GET returns the value of ss if ss is a key and NIL otherwise.

PUT. The loop of lines L9 to L14 takes the same steps as the walk of Lemma 2, but where the walk would reach a NIL link, it creates a node there. A node created by a left or right step at level d+1d + 1 gets the symbol s[d]s[d], and it is put at the place where the BST search for s[d]s[d] ended, so it is larger than each node where the search went right and smaller than each node where it went left. This is a BST insert, and (S1) holds in that sibling tree. A node created by a middle step starts a new sibling tree with one node, where (S1) holds. When the loop reaches line L13, xx is the node with str(x)=sstr(x) = s. Lines L13 and L14 give it the value vv and count it if it was new as a key, so (S2) and (S4) hold. For (S3), each node created in this call lies on the path from the root to xx, and each one other than xx was created just before a step into it, so the nodes created by middle steps have a middle child or are xx; a node created by a left or right step at level d+1d + 1 has the symbol s[d]s[d], so the next step from it is a middle step (or it is xx), which gives it a middle child. The value of xx covers xx.

DELETE. If line L16 returns, ss is not a key by Lemma 2 and (S2), and nothing changes. Else line L17 removes the value of the node xx of ss, so (S2) and (S4) hold for the keys other than ss. Now (S3) can fail only at nodes of the path that lost their value or their middle child. The loop keeps this claim: (S1), (S2) and (S4) hold, and (S3) holds at each node except possibly the current node yy. At yy, if yy has a value or a middle child, (S3) holds everywhere, and the loop returns. Else no node lies below y.midy.mid and yy has no value, so yy carries no key and no other node's string depends on it. Line L20 removes yy from its sibling tree with a BST delete, which keeps the BST order of the other nodes of that sibling tree (this is the correctness of the BST delete) and does not change their strings, since the sibling tree and the symbols of its nodes stay the same. So (S1), (S2) and (S4) still hold. The only node whose links changed in a way that can break (S3) is the parent qq of the sibling tree, when yy was the root of the sibling tree below q.midq.mid and the sibling tree is now empty. Then qq is the next node of the path. If yy was reached by a left or right link, its parent in the path keeps its middle child and its value, so the next round returns. So the claim holds for the next node of the path. The loop ends at the root of the tree at the latest.

KEYS-WITH-PREFIX. If p=εp = \varepsilon, the walk starts at the root with prefix ε\varepsilon. Else, by Lemma 2, line L21 finds the node xx with str(x)=pstr(x) = p or returns the empty list. In the second case no key starts with pp: the node of a key kk with p⪯kp \preceq k has the node of pp as an ancestor through middle links, by the construction of strstr. In the first case, pp is reported if it is a key, and the other keys with prefix pp are the strings of the nodes with a value in the tree below x.midx.mid, by (S2) and the definition of strstr.

The walk of lines L22 to L24 visits each node yy of the given tree once, with the prefix tt such that str(y)=t⋅y.cstr(y) = t \cdot y.c: this holds at the start, a left or right step keeps tt (same sibling tree), and a middle step extends it by y.cy.c (line L24). So line L23 appends exactly the keys of the tree. Order. Within a sibling tree the in-order visit takes the symbols in increasing order, by (S1). At yy, the string str(y)str(y) is reported before the keys below y.midy.mid, which extend it, and all of these come after the keys of the left subtree of yy and before those of the right subtree, whose last symbol at this level is smaller or larger. These are the rules of lexicographic order, so the list is sorted. ■\blacksquare

Back to Ternary search tree