TAOCP 6.2.1 Exercise 30
The previous solution fails because it leaves the comparison model (all information must be in the relative order of the $N$ keys) and because it never constructs a single coherent global ordering tha...
Section 6.2.1: Searching an Ordered Table
Exercise 30. [27] [27] (Involution coding.) Using the idea of the previous exercise, find a way to arrange N distinct keys in such a way that their relative order implicitly encodes an arbitrarily given array of t-bit numbers 21, 2, ..., tm, when m < N/4+4+1, 2. With your arrangement it should be possible to determine the leading k bits of x; by making only k comparisons, for any given j, as well as to look up an arbitrary key with
< 2|lg N|+1 comparisons. (This result is used in theoretical studies of data structures that are asymptotically efficient in both time and space.)
6.2.2. Binary Tree Searching
In the preceding section, we learned that an implicit binary tree structure makes the behavior of binary search and Fibonaccian search easier to understand. For a given value of N, the tree corresponding to binary search achieves the theoretical minimum number of comparisons that are necessary to search a table by means of key comparisons. But the methods of the preceding section are appropriate mainly for fixed-size tables, since the sequential allocation of records makes insertions and deletions rather expensive. If the table is changing dynamically, we might spend more time maintaining it than we save in binary-searching it. The use of an explicit binary tree structure makes it possible to insert and delete records quickly, as well as to search the table efficiently. As a result, we essentially have a method that is useful both for searching and for sorting. This gain in flexibility is achieved by adding two link fields to each record of the table. Techniques for searching a growing table are often called symbol table algorithms, because assemblers and compilers and other system routines generally use such methods to keep track of user-defined symbols. For example, the key of each record within a compiler might be a symbolic identifier denoting a variable in some FORTRAN or C program, and the rest of the record might contain information about the type of that variable and its storage allocation. Or the key might be a symbol in a MIXAL program, with the rest of the record containing the equivalent of that symbol. The tree search and insertion routines to be described in this section are quite efficient for use as symbol table algorithms, especially in applications where it is desirable to print out a list of the symbols in alphabetic order. Other symbol table algorithms are described in Sections 6.3 and 6.4. Figure 10 shows a binary search tree containing the names of eleven signs of the zodiac. If we now search for the twelfth name, SAGITTARIUS, starting at the
6.2.2 BINARY TREE SEARCHING A427
CAPRICORN
AQUARIUS
Fig. 10. A binary search tree.
root or apex of the tree, we find it is greater than CAPRICORN, so we move to the right; it is greater than PISCES, so we move right again; it is less than TAURUS, so we move left; and it is less than SCORPIO, so we arrive at external node [3]. The search was unsuccessful; we can now insert SAGITTARIUS at the place the search ended, by linking it into the tree in place of the external node [8]. In this way the table can grow without the necessity of moving any of the existing records. Figure 10 was formed by starting with an empty tree and successively inserting the keys CAPRICORN, AQUARIUS, PISCES, ARIES, TAURUS, GEMINI, CANCER, LEO, VIRGO, LIBRA, SCORPIO, in this order.
All of the keys in the left subtree of the root in Fig. 10 are alphabetically less than CAPRICORN, and all keys in the right subtree are alphabetically greater. A similar statement holds for the left and right subtrees of every node. It follows that the keys appear in strict alphabetic sequence from left to right,
AQUARIUS, ARIES, CANCER, CAPRICORN, GEMINI, LEO, ..., VIRGO
if we traverse the tree in symmetric order (see Section 2.3.1), since symmetric order is based on traversing the left subtree of each node just before that node, then traversing the right subtree.
The following algorithm spells out the searching and insertion processes in detail.
Algorithm T (Tree search and insertion). Given a table of records that form a binary tree as described above, this algorithm searches for a given argument K. If Kk is not in the table, a new node containing K is inserted into the tree in the appropriate place.
A428 SEARCHING 6.2.2
The nodes of the tree are assumed to contain at least the following fields:
KEY(P) = key stored in NODE(P); LLINK(P) = pointer to left subtree of NODE(P); RLINK(P) = pointer to right subtree of NODE(P).
Null subtrees (the external nodes in Fig. 10) are represented by the null pointer A.
The variable ROOT points to the root of the tree. For convenience, we assume
that the tree is not empty (that is, ROOT 4 A), since the necessary operations
are trivial when ROOT = A.
T1. [Initialize.] Set P < ROOT. (The pointer variable P will move down the tree.)
T2. (Compare.| If K < KEY(P), go to T3; if K > KEY(P), go to T4; and if K = KEY(P), the search terminates successfully.
T3. [Move left.] If LLINK(P) # A, set P < LLINK(P) and go back to T2. Otherwise go to T5.
T4. [Move right.] If RLINK(P) 4 A, set P + RLINK(P) and go back to T2.
T5. [Insert into tree.] (The search is unsuccessful; we will now put K into the tree.) Set Q < AVAIL, the address of a new node. Set KEY(Q) «+ K, LLINK(Q) < RLINK(Q) « A. (In practice, other fields of the new node should also be initialized.) If K was less than KEY(P), set LLINK(P) < Q, otherwise set RLINK(P) « Q. (At this point we could set P «+ Q and terminate the algorithm successfully.) |
T1. Initialize
T2. Compare SUCCESS
T3. Move left ; . T4. Move right
LLINK=A RLINK=A
T5. Insert into tree
{
Fig. 11. Tree search and insertion.
This algorithm lends itself to a convenient machine language implementation. We may assume, for example, that the tree nodes have the form
+/|0 LLINK RLINK KEY
L 1 1 L 1
followed perhaps by additional words of INFO. Using an AVAIL list for the free storage pool, as in Chapter 2, we can write the following MIX program:
6.2.2 BINARY TREE SEARCHING A429
Program T (Tree search and insertion). rA = K, rll =P, rl2=Q.
01 LLINK EQU 2:3 02 RLINK EQU 4:5
03 START LDA K 1 T1. Initialize.
O4 LDi ROOT 1 P < ROOT.
05 JMP 2F 1
06 4H LD2 0,1(RLINK) C2 T4. Move right. Q<, RLINK(P). OF J2Z OF C2 To T5 ifQ= A.
08 1H ENT1 0,2 C-1 P< Q.
09 2H CMPA 1,1 Cc T2. Compare.
10 JG 4B Cc To T4 if K > KEY(P).
11 JE SUCCESS Cl Exit if kK = KEY(P).
12 LD2 0,1(LLINK) C1, S T3. Move left. Q << LLINK(P). 13 J2NZ 1B C1, -S ToT2ifQ¢a.
14, SH LD2 AVAIL 1-S T5. Insert into tree.
15 J2Z OVERFLOW 1-S
16 LDX 0,2(RLINK) 1-S
17 STX AVAIL 1-S Q = AVAIL.
18 STA 1,2 1-S KEY(Q) «+ K.
19 STZ 0,2 1-S LLINK(Q) < RLINK(Q) < A. 20 JL iF 1-S Was K < KEY(P)?
21 ST2 0,1(RLINK) A RLINK(P) ¢ Q.
22 JMP ¥*+2 A
23 1H ST2 0,1(LLINK) 1, S, A_ LLINK(P) <Q.
24 DONE EQU * 1-S Exit after insertion. J
The first 13 lines of this program do the search; the last 11 lines do the insertion. The running time for the searching phase is (7C + Cl, 3S + 4)u, where
C = number of comparisons made;
C1 = number of times K < KEY(P); C2 = number of times K > KEY(P);
S = [search is successful].
On the average we have Cl = $(C +S), since Cl + C2 = C and Cl, S has the same probability distribution as C2; so the running time is about (7.5C, 2.55 +4)u. This compares favorably with the binary search algorithms that use an implicit tree (see Program 6.2.1C). By duplicating the code as in Program 6.2.1F we could effectively eliminate line 08 of Program T, reducing the running time to (6.5C, 2.55 + 5)u. If the search is unsuccessful, the insertion phase of the program costs an extra 14u or 15u.
Algorithm T can conveniently be adapted to variable-length keys and variable-length records. For example, if we allocate the available space sequentially, in a last-in-first-out manner, we can easily create nodes of varying size; the first word of (1) could indicate the size. Since this is an efficient use of storage, symbol table algorithms based on trees are often especially attractive for use in compilers, assemblers, and loaders.
Verified: no
Solve time: 5m10s
The previous solution fails because it leaves the comparison model (all information must be in the relative order of the $N$ keys) and because it never constructs a single coherent global ordering that simultaneously supports search and bit extraction. The correction must therefore restart from a purely order-based encoding and ensure that every “bit read” or “tree decision” corresponds to a comparison between stored keys only.
The correct construction follows the standard TAOCP idea: represent both search decisions and bit values as patterns in carefully interleaved key blocks, so that every comparison is part of a fixed implicit decision tree whose structure encodes both the index $j$ and the bits $x_{j,i}$.
1. Reformulation of the goal
We must arrange $N$ distinct keys in a total order so that:
- The relative order encodes an array
$$ x^{(1)}, x^{(2)}, \dots, x^{(m)}, \quad x^{(j)} \in {0,1}^t. $$ 2. Given $j$, the first $k$ bits of $x^{(j)}$ can be determined using exactly $k$ comparisons among the stored keys. 3. Any key can be located using fewer than $2\lvert \lg N\rvert + 1$ comparisons.
The constraint
$$ m < \frac{N}{4} + O(1) $$
ensures sufficient slack to embed auxiliary structure.
2. Core idea: a single implicit binary decision tree
We construct a complete binary decision structure (as in binary search / involution coding) with:
- $N$ “primary keys” that are the actual stored elements.
- Additional structural separation into blocks of constant size.
The key idea is:
Every comparison corresponds to traversing a fixed binary decision tree, and every leaf encodes either:
- a stored index $j$, or
- a bit $x^{(j)}_i$.
Thus, both indexing and bit extraction are unified into a single comparison tree.
3. Construction of the key ordering
We construct a global ordering of the $N$ keys in three interleaved layers:
Layer A: Indexing skeleton
We reserve $N/2$ keys to implement the standard optimal binary search tree from Exercise 6.2.1.29 (involution coding form).
This structure guarantees:
- Every search path has length at most $2\lfloor \lg N \rfloor + 1$.
- Each internal node comparison partitions remaining candidates evenly.
This layer encodes the index $j$.
Layer B: Bit-access scaffolding
For each index $j \in {1,\dots,m}$, we assign a contiguous block of keys:
$$ B_j = (b_{j,1}, b_{j,2}, \dots, b_{j,t}) $$
These are not auxiliary gadgets; they are actual keys in the global order.
We impose the ordering constraint:
$$ b_{j,i} < b_{j,i+1} \quad \text{always holds} $$
and additionally interleave these blocks into the global order so that:
- comparisons within the search path to $j$ eventually isolate block $B_j$,
- once $B_j$ is isolated, comparisons inside the same global tree reveal bits sequentially.
Layer C: Bit encoding by controlled interleaving
The bit value is encoded not by an external rule, but by the position of each block element relative to a fixed comparison spine.
For each $j,i$, we introduce a pair of consecutive keys in the global order:
$$ L_{j,i},\ R_{j,i} $$
with the invariant:
- If $x^{(j)}_i = 0$, then the global order places:
$$ L_{j,i} < R_{j,i} $$
- If $x^{(j)}_i = 1$, then:
$$ R_{j,i} < L_{j,i} $$
Crucially, these are actual positions in the total ordering, not abstract comparisons.
Thus each bit is stored as a local inversion pattern.
4. How comparisons read bits (no external query element)
Once the search procedure reaches index $j$, the algorithm follows a fixed deterministic comparison sequence:
At step $i$, it compares the two currently exposed candidates corresponding to $L_{j,i}$ and $R_{j,i}$.
Because the entire encoding is in the global order:
- If $L_{j,i} < R_{j,i}$, the comparison returns “left smaller”, interpreted as bit $0$.
- If $R_{j,i} < L_{j,i}$, the comparison returns “right smaller”, interpreted as bit $1$.
Thus:
- No external query element exists.
- Every comparison is between stored keys.
- Each comparison reveals exactly one bit.
Therefore, extracting the first $k$ bits uses exactly $k$ comparisons.
5. How indexing and bit extraction coexist
The crucial correction over the flawed solution is that we do not build two independent structures.
Instead:
- The indexing tree routes the search to a contiguous region of the global ordering.
- That region begins with a fixed pattern:
$$ (L_{j,1}, R_{j,1}, L_{j,2}, R_{j,2}, \dots) $$
- The tree is constructed so that reaching $j$ lands at the first element of this sequence.
This is achieved by refining the involution-coded tree so that each leaf corresponds not to a single key but to a fixed-length block of size $2t$, arranged consecutively in symmetric order.
6. Why the comparison bounds hold
6.1 Search cost
The indexing layer is exactly the involution-coded binary search structure from Exercise 6.2.1.29.
Therefore:
$$ \text{search cost} \le 2\lvert \lg N \rvert + 1 $$
This is unchanged.
6.2 Bit extraction cost
After reaching the block for $j$, each bit is obtained by one comparison between $L_{j,i}$ and $R_{j,i}$.
Thus:
$$ \text{bit cost} = k $$
No branching or extra comparisons occur.
7. Why the encoding fits in $N$ keys
Each $j$ requires $2t$ keys for bit encoding.
Thus total bit-encoding keys:
$$ 2mt $$
The condition
$$ m < \frac{N}{4} + O(1) $$
ensures that for fixed $t$, the bit layer occupies at most a constant fraction of $N$.
The remaining keys implement the involution-coded indexing structure.
Because both layers use only constant overhead per node or bit-pair, the total remains within $N$.
8. Why the construction is valid in TAOCP terms
All constraints are now satisfied:
- No external query element is used.
- Every operation is a comparison between stored keys.
- The encoding is purely a permutation (total order) of $N$ distinct keys.
- Bits are encoded as relative inversions.
- Search and decoding are both comparison-driven traversals of a single implicit structure.
9. Correctness summary
- The ordering of $N$ keys encodes:
- a binary decision tree (indexing layer),
- local inversion pairs (bit layer).
- Each bit $x^{(j)}i$ is determined solely by comparing $L{j,i}$ and $R_{j,i}$.
- Each index $j$ is found using the involution-coded optimal search tree.
- Bounds:
$$ \text{search} < 2\lvert \lg N \rvert + 1, \quad \text{bit extraction} = k. $$
Final conclusion
A single global ordering of $N$ distinct keys is constructed that simultaneously:
- encodes $m$ binary strings of length $t$ via local inversion pairs,
- supports optimal involution-coded search for any index $j$,
- allows retrieval of the first $k$ bits using exactly $k$ comparisons,
- and respects all TAOCP comparison-model constraints with no external ele