Trie-based search as described in has two inconvenient flaws. First, the one-way branching leads to the creation of extra nodes in the trie, which seem unnecessary. Second, there are two different types of nodes in the trie, which leads to complications (see ). In 1968, Morrison discovered a way to avoid both of these problems, in a method that he named patricia ("practical algorithm to retrieve information coded in alphanumeric"). Morrison developed his algorithm in the context of string-indexing apps of the type that we shall consider in , but it is equally effective as a symbol-table implementation. Like DSTs, patricia tries allow search for N keys in a tree with just N nodes; like tries, they require only about lg N bit comparisons and one full key comparison per search, and they support other ADT operations. Moreover, these performance characteristics are independent of key length, and the data structure is suitable for variable-length keys.

Patricia trie symbol-table implementation

Nodes in patricia tries contain a field that indicates which bit position distinguishes keys on the right from keys on the left. We use a dummy node head at the top of the trie that is always the result of a search for the null (all 0s) key. The root of the trie is at head.l (the link head.r is unused).

class ST { private class Node { ITEM item; Node l, r; int bit; Node(ITEM x, int i) { item = x; bit = i; } } private Node head; ST(int maxN) { head = new Node(null, -1); head.l = head; } ITEM search(KEY key) // See Program 15.6 void insert(ITEM x) // See Program 15.7 public String toString() // See Program 15.8 } 

Starting with the standard trie data structure, we avoid one-way branching via a simple device: we put into each node the index of the bit to be tested to decide which path to take out of that node. Thus, we jump directly to the bit where a significant decision is to be made, bypassing the bit comparisons at nodes where all the keys in the subtree have the same bit value. Moreover, we avoid external nodes via another simple device: we store data in internal nodes and replace links to external nodes with links that point back upwards to the correct internal node in the trie. These two changes allow us to represent tries with binary trees comprising nodes with a key and two links (and an additional field for the index), which we call patricia tries. With patricia tries, we store keys in nodes as with DSTs, and we traverse the tree according to the bits of the search key; but we do not use the keys in the nodes on the way down the tree to control the search; we merely store them there for possible later reference, when the bottom of the tree is reached.

Patricia-trie search

The recursive method searchR returns the unique node that could contain the record with key v. It travels down the trie, using the bits of the tree to control the search, but tests only 1 bit per node encountered-the one indicated in the bit field. It terminates the search when it encounters an external link, one which points up the tree. The search method search calls searchR, then tests the key in that node to determine whether the search is a hit or a miss.

private ITEM searchR(Node h, KEY v, int i) { if (h.bit <= i) return h.item; if (bit(v, h.bit) == 0) return searchR(h.l, v, h.bit); else return searchR(h.r, v, h.bit); } ITEM search(KEY key) { ITEM t = searchR(head.l, key, -1); if (t == null) return null; if (equals(t.key(), key)) return t; return null; } 

As hinted in the previous paragraph, it is easier to follow the mechanics of the algorithm if we first take note that we can regard standard tries and patricia tries as different representations of the same abstract trie structure. For example, the tries in and at the top in , which illustrate search and insertion for patricia tries, represent the same abstract structure as do the tries in . The search and insertion algorithms for patricia tries use, build, and maintain a concrete representation of the abstract trie data structure different from the search and insertion algorithms discussed in , but the underlying trie abstraction is the same.

Screenshot Patricia search

In a successful search for R = 10010 in this sample patricia trie (top), we move right (since bit 0 is 1), then left (since bit 4 is 0), which brings us to R (the only key in the tree that begins with 1***0). On the way down the tree, we check only the key bits indicated in the numbers over the nodes (and ignore the keys in the nodes). When we first reach a link that points up the tree, we compare the search key against the key in the node pointed to by the up link, since that is the only key in the tree that could be equal to the search key. In an unsuccessful search for I = 01001, we move left at the root (since bit 0 of the key is 0), then take the right (up) link (since bit 1 is 1) and find that H (the only key in the trie that begins with 01) is not equal to I.

Java graphics 15fig10

Screenshot Patricia-trie insertion

To insert I into the sample patricia trie in , we add a new node to check bit 4, since H = 01000 and I = 01001 differ in only that bit (top). On a subsequent search in the trie that comes to the new node, we want to check H (left link) if bit 4 of the search key is 0; if the bit is 1 (right link), the key to check is I. To insert N = 01110 (bottom), we add a new node in between H and I to check bit 2, since that bit distinguishes N from H and I.

Java graphics 15fig11

is an implementation of the patricia-trie search algorithm. The method differs from trie search in three ways: there are no explicit null links, we test the indicated bit in the key instead of the next bit, and we end with a search key comparison at the point where we follow a link up the tree. It is easy to test whether a link points up, because the bit indices in the nodes (by definition) increase as we travel down the tree. To search, we start at the root and proceed down the tree, using the bit index in each node to tell us which bit to examine in the search key-we go right if that bit is 1, left if it is 0. The keys in the nodes are not examined at all on the way down the tree. Eventually, an upward link is encountered: each upward link points to the unique key in the tree that has the bits that would cause a search to take that link. Thus, if the key at the node pointed to by the first upward link encountered is equal to the search key, then the search is successful; otherwise, it is unsuccessful.

illustrates search in a patricia trie. For a miss due to the search taking a null link in a trie, the corresponding patricia trie search will take a course somewhat different from that of standard trie search, because the bits that correspond to one-way branching are not tested at all on the way down the trie. For a search ending at a leaf in a trie, the patricia-trie search ends up comparing against the same key as the trie search but without examining the bits corresponding to one-way branching in the trie.

The implementation of insertion for patricia tries mirrors the two cases that arise in insertion for tries, as illustrated in . As usual, we gain information on where a new key belongs from a search miss. For tries, the miss can occur either because of a null link or because of a key mismatch at a leaf. For patricia tries, we need to do more work to decide which type of insertion is needed, because we skipped the bits corresponding to one-way branching during the search. A patricia-trie search always ends with a key comparison, and this key carries the information that we need. We find the leftmost bit position where the search key and the key that terminated the search differ, then search through the trie again, comparing that bit position against the bit positions in the nodes on the search path. If we come to a node that specifies a bit position higher than the bit position that distinguishes the key sought and the key found, then we know that we skipped a bit in the patricia-trie search that would have led to a null link in the corresponding trie search, so we add a new node for testing that bit. If we never come to a node that specifies a bit position higher than the one that distinguishes the key sought and the key found, then the patricia-trie search corresponds to a trie search ending in a leaf, and we add a new node that distinguishes the search key from the key that terminated the search. We always add just one node, which references the leftmost bit that distinguishes the keys, where standard trie insertion might add multiple nodes with one-way branching before reaching that bit. That new node, besides providing the bit-discrimination that we need, will also be the node that we use to store the new item.

We use the convention that the leftmost link (the one corresponding to a key that is all 0 bits) does not point to any internal node. We need such a convention because the number of external links exceeds the number of internal nodes by precisely 1 in every binary tree. To make sure that no search ever follows that link, we further adopt the convention for class types that only the null key has all 0 bits. This convention is easy to enforce by implementing bit so as to always return 0 for the null key and to return 1 after exhausting the bits of any non-null key (see ). shows the initial stages of the construction of a sample trie, which illustrate these conventions.

Screenshot Patricia-trie construction

This sequence depicts the result of inserting the keys A S E R C H into an initially empty patricia trie. depicts the result of inserting I and then N into the tree at the bottom.

Java graphics 15fig12

is an implementation of the patricia-trie-insertion algorithm. The code follows directly from the description in the previous paragraph, with the additional observation that we view links to nodes with bit indices that are not larger than the current bit index as links to external nodes. The insertion code merely tests this property of the links, but it does not have to move keys or links around at all. The upward links in patricia tries seem mysterious at first, but the decisions about which links to use when each node is inserted are surprisingly straightforward. The end result is that using one node type rather than two simplifies the code substantially.

Patricia-trie insertion

To insert a key into a patricia trie, we begin with a search. The method searchR from gets us to a unique key in the tree that must be distinguished from the key to be inserted. We determine the leftmost bit position at which this key and the search key differ, then use the recursive method insertR to travel down the tree and to insert a new node containing v at that point.

In insertR, there are two cases, corresponding to the two cases illustrated in . The new node could replace an internal link (if the search key differs from the key found in a bit position that was skipped) or an external link (if the bit that distinguishes the search key from the found key was not needed to distinguish the found key from all the other keys in the trie).

This code assumes that KEY is a class type and depends upon bit being implemented so that null is the only key that is all 0s (see text).

private Node insertR(Node h, ITEM x, int i, Node p) { KEY v = x.key(); if ((h.bit >= i) || (h.bit <= p.bit)) { Node t = new Node(x, i); t.l = bit(v, t.bit) == 0 ? t : h; t.r = bit(v, t.bit) == 0 ? h : t; return t; } if (bit(v, h.bit) == 0) h.l = insertR(h.l, x, i, h); else h.r = insertR(h.r, x, i, h); return h; } void insert(ITEM x) { inti=0; KEY v = x.key(); ITEM t = searchR(head.l, v, -1); KEYw=(t==null) ? null : t.key(); if (v == w) return; while (bit(v, i) == bit(w, i)) i++; head.l = insertR(head.l, x, i, head); } 

Patricia-trie sort

This recursive procedure shows the records in a patricia trie in order of their keys. We imagine the items to be in (virtual) external nodes, which we can identify by testing when the bit index on the current node is not larger than the bit index on its parent. Otherwise, this program is a standard inorder traversal.

private String toStringR(Node h, int i) { if (h == head) return ""; if (h.bit <= i) return h.item + "\n"; return toStringR(h.l, h.bit) + toStringR(h.r, h.bit); } public String toString() { return toStringR(head.l, -1); } 

By construction, all external nodes below a node with bit index k begin with the same k bits (otherwise, we would have created a node with bit index less than k to distinguish two of them). Therefore, we can convert a patricia trie to a standard trie by creating the appropriate internal nodes between nodes where bits are skipped and by replacing links that point up the tree with links to external nodes (see ). However, does not quite hold for patricia tries, because the assignment of keys to internal nodes does depend on the order in which the keys are inserted. The structure of the internal nodes is independent of the key-insertion order, but external links and the placement of the key values are not.

An important consequence of the fact that a patricia trie represents an underlying standard trie structure is that we can use a recursive inorder traversal to visit the nodes in order, as demonstrated in the implementation given in . We visit just the external nodes, which we identify by testing for nonincreasing bit indices.

Patricia is the quintessential radix search method: it manages to identify the bits that distinguish the search keys and to build them into a data structure (with no surplus nodes) that quickly leads from any search key to the only key in the data structure that could be equal to the search key. shows the patricia trie for the same keys used to build the trie of -the patricia trie not only has 44 percent fewer nodes than the standard trie but also is nearly perfectly balanced.

Screenshot Patricia-trie example

This patricia trie, built by insertion of about 200 random keys, is equivalent to the trie of , with one-way branching removed. The resulting tree is nearly perfectly balanced.

Java graphics 15fig13

Property 15.5

Insertion or search for a random key in a patricia trie built from N random bitstrings requires about lg N bit comparisons on the average, and about 2 lg N bit comparisons in the worst case. The number of bit comparisons is never more than the length of the key.

This fact is an immediate consequence of , since paths in patricia tries are no longer than paths in the corresponding trie. The precise average-case analysis of patricia is difficult; it turns out that patricia involves one fewer comparison, on the average, than does a standard trie (see reference section). Screenshot

gives empirical data supporting the conclusion that DSTs, standard binary tries, and patricia tries have comparable performance (and that they provide search times comparable to or shorter than the balanced-tree methods of ) when keys are integers, and certainly should be considered for symbol-table implementations even with keys that can be represented as short bitstrings, taking into account the various straightforward tradeoffs that we have noted.

Note that the search cost given in does not grow with the key length. By contrast, the search cost in a standard trie typically does depend on the length of the keys-the first bit position that differs in two given keys could be arbitrarily far into the key. All the comparison-based search methods that we have considered also depend on the key length-if two keys differ in only their rightmost bit, then comparing them requires time proportional to their length. Furthermore, hashing methods always require time proportional to the key length for a search in order to compute the hash function. But patricia immediately takes us to the bits that matter and typically involves testing less than lg N of them. This effect makes patricia (or trie search with one-way branching removed) the search method of choice when the search keys are long.

Table 15.1. Empirical study of trie implementations

These relative timings for construction and search in symbol tables with random sequences of 32-bit integers confirm that digital methods are competitive with balanced-tree methods, even for keys that are random bits. Performance differences are more remarkable when keys are long and are not necessarily random (see ), or when careful attention is paid to making the key-bit-access code efficient (see ).

construction

search hits

N R D T P R D T P
1250 25 14 14 16 5 3 4 3
2500 43 23 29 29 9 7 6 5
5000 70 43 43 60 19 15 12 11
12500 116 96 100 102 59 47 36 32
25000 298 204 251 305 147 115 87 80
50000 1120 464 476 604 356 290 198 180
100000 2476 1189 1172 1411 853 665 456 429
200000 3591 4487 2505 3240 1884 1579 1012 945

Key:

R Red-black BST (Programs and )

D DST ()

T Trie (Programs and )

P Patricia trie (Programs and )

For example, suppose that we have a computer that can efficiently access 8-bit bytes of data, and we have to search among millions of 1000-bit keys. In this case, patricia would require accessing only about 20 bytes of the search key for the search, plus one 125-byte equality comparison; in contrast, hashing would require accessing all 125 bytes of the search key to compute the hash function (plus a few equality comparisons), and comparison-based methods would require 20 to 30 full key comparisons. It is true that key comparisons, particularly in the early stages of a search, require only a few byte comparisons, but later stages typically involve many more bytes. We shall consider comparative performance of various methods for searching with lengthy keys again in .

Indeed, there needs to be no limit at all on the length of search keys for patricia. Patricia is particularly effective in apps with variable-length keys that are potentially huge, such as the one discussed in . With patricia, we generally can expect that the number of bit inspections required for a search among N records, even with huge keys, will be roughly proportional to lg N.

Exercises

Modify the implementation of the two-parameter bit method in the text after to return 1 if its second parameter is not less than bitsword and to always return 0 if its first parameter is null.

What happens when you use to insert a record whose key is equal to some key already in the trie?

Java graphics icon01 15.36 Draw the patricia trie that results when you insert the keys E A S Y Q U T I O N in that order into an initially empty trie.

Java graphics icon01 15.37 Draw the patricia trie that results when you insert the keys 01010011 01001010 in that order into an initially empty trie.

ScreenshotDraw the patricia trie that results when you insert the keys 01001010 01010011 in that order into an initially empty trie.

Run empirical studies to compare the height and internal path length of a patricia trie built by insertion of N random 32-bit keys into an initially empty trie with the same measures of a standard binary search tree and a red-black tree () built from the same keys, for N = 103, 104, 105, and 106 (see Exercises and ).

Give a full characterization of the worst-case internal path length of a patricia trie with N distinct w-bit keys.

Implement a lazy count operation for the patricia-based symbol table implementation of Programs through .

Add an integer field N to Node and modify the patricia code in Programs through to implement an eager count operation that takes constant time.

Implement the select operation for a patricia-based symbol table.

Java graphics roundbullet 15.44 Implement the remove operation for a patricia-based symbol table.

Java graphics roundbullet 15.45 Implement the join operation for patricia-based symbol tables.

ScreenshotWrite a program that prints out all keys in a patricia trie that have the same initial t bits as a given search key.

Modify standard trie search and insertion (Programs and ) to eliminate one-way branching in the same manner as for patricia tries. If you have done , start with that program instead.

Modify patricia search and insertion (Programs and ) to maintain a table of 2r tries, as described in .

Show that each key in a patricia trie is on its own search path and is therefore encountered on the way down the tree during a search operation as well as at the end.

Modify patricia search () to compare keys on the way down the tree to improve search-hit performance. Run empirical studies to evaluate the effectiveness of this change (see ).

Use a patricia trie to build a data structure that can support an existence table ADT for w-bit integers (see ).

Java graphics roundbullet 15.52 Write programs that convert a patricia trie to a standard trie on the same keys, and vice versa.