Channels ▼
RSS

.NET

Ternary Search Trees

Source Code Accompanies This Article. Download It Now.


Partial-Match Searching

We turn next to the more subtle problem of "partial-match" or "crossword puzzle" searching: A query string may contain both regular letters and the "don't care" character ".". Searching the dictionary for the pattern ".u.u.u" matches the single word auhuhu, while the pattern ".a.a.a" matches 94 words, including banana, casaba, and pajama. (That pattern does not match abracadabra, though. The entire word must match the pattern; we do not look for patterns in the middle of longer words.)

This venerable problem has been studied in many papers, such as in "The World's Fastest Scrabble Program," by A.W. Appel and G.J. Jacobson (Communications of the ACM, May 1988). In "Partial-Match Retrieval Algorithms," (SIAM Journal on Computing, 5, 1976), R.L. Rivest presents an algorithm for partial-match searching in digital tries: Take the single given branch if a letter is specified, for a don't-care character, recursively search all branches. Function pmsearch implements Rivest's method in ternary search trees. The function puts pointers to matching words in srcharr[0..srchtop-1]. It is called, for instance, by:

srchtop = 0; 
pmsearch(root, ".a.a.a"); 

The following is the complete search code:

void pmsearch(Tptr p, char *s) 
{    if  (!p) return; 
     nodecnt++; 
     if (*s == '.' || *s < p->splitchar) 
        pmsearch(p->lokid, s); 
     if (*s == '.' || *s == p->splitchar) 
        if (p->splitchar && *s) 
            pmsearch(p->eqkid, s+1); 
     if (*s == 0 && p->splitchar == 0) 
            srcharr[srchtop++] = 
                (char *) p->eqkid; 
     if (*s == '.' || *s > p->splitchar) 
            pmsearch(p->hikid, s); 
}

Function pmsearch has five if statements. The second and fifth if statements are symmetric; they recursively search the lokid (or hikid) when the search character is the don't care "." or when the search string is less (or greater) than the splitchar. The third if statement recursively searches the eqkid if both the splitchar and string are nonnull. The fourth if statement detects a match to the query and adds the pointer to the complete word (stored in eqkid by our sleazy hack).

Rivest states that partial-match search in a trie requires "time about O(n(k-s)/k) to respond to a query word with s letters specified, given a file of n k-letter words." Ternary search trees can be viewed as an implementation of his tries (with binary trees implementing multiway branching), so we expected his results to apply immediately to our program. Our experiments, however, led to a surprise: Unspecified positions at the front of the query word are dramatically more costly than unspecified characters at the end of the word. (Rivest suggested shuffling the characters in a word to avoid such problems.) iTable 3i gives the flavor of our experiments. The first line says that the search visited 18 nodes to find the single match to the pattern "banana." The tree contains a total of 1,026,033 nodes; searches with some of the first letters specified visit just a tiny fraction of those nodes.

Table 3: Partial-match searching.

Near-Neighbor Searching

We finally examine the problem of "near-neighbor searching" in a set of strings: We are to find all words in the dictionary that are within a given Hamming distance of a query word. For instance, a search for all words within distance two of Dobbs finds Debby, hobby, and 14 other words.

Function nearsearch performs a near-neighbor search in a ternary search tree. Its three arguments are a tree node, string, and distance. The first if statement returns if the node is null or the distance is negative. The second and fourth if statements are symmetric: They search the appropriate child if the distance is positive or if the query character is on the appropriate side of splitchar. The third if statement either checks for a match or recursively searches the middle child.

void nearsearch(Tptr p, char *s, int d) 
{   if (!p || d < 0) return; 
    if (d > 0 || *s < p->splitchar) 
        nearsearch(p->lokid, s, d); 
    if (p->splitchar == 0) { 
       if ((int) strlen(s) <= d) 
          srcharr[srchtop++] = (char *) p->eqkid; 
    } else 
       nearsearch(p->eqkid, *s ? s+1:s, 
          (*s == p->splitchar) ? d:d-1); 
    if (d > 0 || *s > p->splitchar) 
        nearsearch(p->hikid, s, d); 
} 

Our web site contains data on the performance of this search algorithm. It is quite efficient when searching for near neighbors, but searching for distant neighbors grows more expensive. A simple probabilistic model accurately predicts its run time on real data.

Conclusion

The primary challenge in implementing digital search tries is to avoid using excessive memory for trie nodes that are nearly empty. Ternary search trees may be viewed as a trie implementation that gracefully adapts to handle this case, at the cost of slightly more work for full nodes. Ternary search trees combine the best of two worlds: the low space overhead of binary search trees and the character-based time efficiency of digital search tries.

Ternary search trees have been used for several years to represent English dictionaries in a commercial optical character recognition (OCR) system built at Bell Labs. The trees were faster than hashing for the task, and they gracefully handle the 34,000-character set of the Unicode Standard. The designers have also experimented with using partial-match for word lookup: Replace letters with low probability of recognition with the "don't care" character.

Ternary search trees are efficient and easy to implement. They offer substantial advantages over both binary search trees and digital search tries. We feel that they are superior to hashing in many applications for the following reasons:

  • Ternary trees do not incur extra overhead for insertion or successful searches.

  • Ternary trees are usually substantially faster than hashing for unsuccessful searches.
  • Ternary trees gracefully grow and shrink; hash tables need to be rebuilt after large size changes.
  • Ternary trees support advanced searches, such as partial-match and near-neighbor search.
  • Ternary trees support many other operations, such as traversal to report items in sorted order.

Jon is a Member of Technical Staff at Bell Labs. Bob is the William O. Baker Professor of Computer Science at Princeton University. They can be reached at jlb@research.bell-labs.com and rs@cs.princeton.edu, respectively.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video