Trie
In computer science, a trie, also called digital tree or prefix tree, is a kind of search tree—an ordered tree data structure used to store a dynamic set or associative array where the keys are usually strings. Unlike a binary search tree, no node in the tree stores the key associated with that node; instead, its position in the tree defines the key with which it is associated; i.e., the value of the key is distributed across the structure. All the descendants of a node have a common prefix of the string associated with that node, and the root is associated with the empty string. Keys tend to be associated with leaves, though some inner nodes may correspond to keys of interest. Hence, keys are not necessarily associated with every node. For the space-optimized presentation of prefix tree, see compact prefix tree.
In the example shown, keys are listed in the nodes and values below them. Each complete English word has an arbitrary integer value associated with it. A trie can be seen as a tree-shaped deterministic finite automaton. Each finite language is generated by a trie automaton, and each trie can be compressed into a deterministic acyclic finite state automaton.
Though tries can be keyed by character strings, they need not be. The same algorithms can be adapted to serve similar functions on ordered lists of any construct; e.g., permutations on a list of digits or shapes. In particular, a bitwise trie is keyed on the individual bits making up any fixed-length binary datum, such as an integer or memory address.
History and etymology
Tries were first described by René de la Briandais in 1959. The term trie was coined two years later by Edward Fredkin, who pronounces it , after the middle syllable of retrieval. However, other authors pronounce it , in an attempt to distinguish it verbally from "tree".Applications
As a replacement for other data structures
As discussed below, a trie has a number of advantages over binary search trees.A trie can also be used to replace a hash table, over which it has the following advantages:
- Looking up data in a trie is faster in the worst case, O time, compared to an imperfect hash table. An imperfect hash table can have key collisions. A key collision is the hash function mapping of different keys to the same position in a hash table. The worst-case lookup speed in an imperfect hash table is O time, but far more typically is O, with O time spent evaluating the hash.
- There are no collisions of different keys in a trie.
- Buckets in a trie, which are analogous to hash table buckets that store key collisions, are necessary only if a single key is associated with more than one value.
- There is no need to provide a hash function or to change hash functions as more keys are added to a trie.
- A trie can provide an alphabetical ordering of the entries by key.
- Trie lookup can be slower than hash table lookup, especially if the data is directly accessed on a hard disk drive or some other secondary storage device where the random-access time is high compared to main memory.
- Some keys, such as floating point numbers, can lead to long chains and prefixes that are not particularly meaningful. Nevertheless, a bitwise trie can handle standard IEEE single and double format floating point numbers.
- Some tries can require more space than a hash table, as memory may be allocated for each character in the search string, rather than a single chunk of memory for the whole entry, as in most hash tables.
Dictionary representation
Tries are also well suited for implementing approximate matching algorithms, including those used in spell checking and hyphenation software.
Term indexing
A discrimination tree term index stores its information in a trie data structure.Algorithms
The trie is a tree of nodes which supports Find and Insert operations. Find returns the value for a key string, and Insert inserts a string and a value into the trie. Both Insert and Find run in time, where m is the length of the key.A simple Node class can be used to represent nodes in the trie:
class Node:
def __init__ -> None:
# Note that using a dictionary for children
# would not by default lexicographically sort the children, which is
# required by the lexicographic sorting mentioned in the next section
#.
self.children: Dict = # mapping from character to Node
self.value: Optional = None
Note that
children
is a dictionary of characters to a node's children; and it is said that a "terminal" node is one which represents a complete string.A trie's value can be looked up as follows:
def find -> Optional:
"""Find value by key in node."""
for char in key:
if char in node.children:
node = node.children
else:
return None
return node.value
A slight modifications of this routine can be utilized
- to check if there is any word in the trie that starts with a given prefix, and
- to return the deepest node corresponding to some prefix of a given string.
def insert -> None:
"""Insert key/value pair into node."""
for char in key:
if char not in node.children:
node.children = Node
node = node.children
node.value = value
Sorting
Lexicographic sorting of a set of keys can be accomplished by building a trie from them, with the children of each node sorted lexicographically, and traversing it in pre-order, printing only the leaves' values. This algorithm is a form of radix sort.A trie is the fundamental data structure of Burstsort, which was the fastest known string sorting algorithm due to its efficient cache use. Now there are faster ones.
Full-text search
A special kind of trie, called a suffix tree, can be used to index all suffixes in a text in order to carry out fast full text searches.Implementation strategies
There are several ways to represent tries, corresponding to different trade-offs between memory use and speed of the operations. The basic form is that of a linked set of nodes, where each node contains an array of child pointers, one for each symbol in the alphabet. This is simple but wasteful in terms of memory: using the alphabet of bytes and four-byte pointers, each node requires a kilobyte of storage, and when there is little overlap in the strings' prefixes, the number of required nodes is roughly the combined length of the stored strings. Put another way, the nodes near the bottom of the tree tend to have few children and there are many of them, so the structure wastes space storing null pointers.The storage problem can be alleviated by an implementation technique called alphabet reduction, whereby the original strings are reinterpreted as longer strings over a smaller alphabet. E.g., a string of bytes can alternatively be regarded as a string of four-bit units and stored in a trie with sixteen pointers per node. Lookups need to visit twice as many nodes in the worst case, but the storage requirements go down by a factor of eight.
An alternative implementation represents a node as a triple and links the children of a node together as a singly linked list: points to the node's first child, to the parent node's next child. The set of children can also be represented as a binary search tree; one instance of this idea is the ternary search tree developed by Bentley and Sedgewick.
Another alternative in order to avoid the use of an array of 256 pointers, as suggested before, is to store the alphabet array as a bitmap of 256 bits representing the ASCII alphabet, reducing dramatically the size of the nodes.
Bitwise tries
Bitwise tries are much the same as a normal character-based trie except that individual bits are used to traverse what effectively becomes a form of binary tree. Generally, implementations use a special CPU instruction to very quickly find the first set bit in a fixed length key. This value is then used to index a 32- or 64-entry table which points to the first item in the bitwise trie with that number of leading zero bits. The search then proceeds by testing each subsequent bit in the key and choosingchild
or child
appropriately until the item is found.Although this process might sound slow, it is very cache-local and highly parallelizable due to the lack of register dependencies and therefore in fact has excellent performance on modern out-of-order execution CPUs. A red-black tree for example performs much better on paper, but is highly cache-unfriendly and causes multiple pipeline and TLB stalls on modern CPUs which makes that algorithm bound by memory latency rather than CPU speed. In comparison, a bitwise trie rarely accesses memory, and when it does, it does so only to read, thus avoiding SMP cache coherency overhead. Hence, it is increasingly becoming the algorithm of choice for code that performs many rapid insertions and deletions, such as memory allocators. The worst case of steps for lookup is the same as bits used to index bins in the tree.
Alternatively, the term "bitwise trie" can more generally refer to a binary tree structure holding integer values, sorting them by their binary prefix. An example is the x-fast trie.
Compressing tries
Compressing the trie and merging the common branches can sometimes yield large performance gains. This works best under the following conditions:- The trie is static, so that no key insertions to or deletions are required.
- Only lookups are needed.
- The trie nodes are not keyed by node-specific data, or the nodes' data are common.
- The total set of stored keys is very sparse within their representation space.
Such compression is also used in the implementation of the various fast lookup tables for retrieving Unicode character properties. These could include case-mapping tables, or lookup tables normalizing the combination of base and combining characters. For such applications, the representation is similar to transforming a very large, unidimensional, sparse table into a multidimensional matrix of their combinations, and then using the coordinates in the hyper-matrix as the string key of an uncompressed trie to represent the resulting character. The compression will then consist of detecting and merging the common columns within the hyper-matrix to compress the last dimension in the key. For example, to avoid storing the full, multibyte Unicode code point of each element forming a matrix column, the groupings of similar code points can be exploited. Each dimension of the hyper-matrix stores the start position of the next dimension, so that only the offset need be stored. The resulting vector is itself compressible when it is also sparse, so each dimension can be compressed separately.
Some implementations do support such data compression within dynamic sparse tries and allow insertions and deletions in compressed tries. However, this usually has a significant cost when compressed segments need to be split or merged. Some tradeoff has to be made between data compression and update speed. A typical strategy is to limit the range of global lookups for comparing the common branches in the sparse trie.
The result of such compression may look similar to trying to transform the trie into a directed acyclic graph, because the reverse transform from a DAG to a trie is obvious and always possible. However, the shape of the DAG is determined by the form of the key chosen to index the nodes, in turn constraining the compression possible.
Another compression strategy is to "unravel" the data structure into a single byte array.
This approach eliminates the need for node pointers, substantially reducing the memory requirements. This in turn permits memory mapping and the use of virtual memory to efficiently load the data from disk.
One more approach is to "pack" the trie. Liang describes a space-efficient implementation of a sparse packed trie applied to automatic hyphenation, in which the descendants of each node may be interleaved in memory.