Tries

Tries are one of the lesser known data structures. When you look up Trie on wikipedia, you'll see an imposing picture and long sample code. But let's try to make an informal definition, and come up with some simple code to see what actually happens.

Dictionary Based Tries

In Python, a dictionary based trie is both simple to understand, space efficient and fast. Let's look at the code and try to understand it:

class DictBasedTrie(object):

    # the constructor creates an empty trie
    def __init__(self):
        self.data = {}

    # add a word by adding each character on different levels
    def Add(self, word):

        # k is the current level
        k = self.data
        
        # for each character in the word
        for c in word:

            # if we've seen this character on this level before, use it
            try:
                k = k[c]
            except KeyError:
                # we haven't => create a new sub dict
                k[c] = {}
                k = k[c]

To see how this works, let's take the following ingenious sentence that has its own wikipedia article:

James while John had had had had had had had had had had had a better effect on the teacher

Let's write a small test code to add the words in this sentence to a trie, and then look at the trie:

#! -*- Encoding: Latin-1 -*-
...
def selftest():
    t = DictBasedTrie()
    for word in ("James while John had had had had had had had had had had had a better effect on the teacher").split():
        t.Add(word.lower())
        
    pprint.pprint(t.data)

if __name__ == "__main__":
    selftest()

This code outputs the following:

{'a': {},
 'b': {'e': {'t': {'t': {'e': {'r': {}}}}}},
 'e': {'f': {'f': {'e': {'c': {'t': {}}}}}},
 'h': {'a': {'d': {}}},
 'j': {'a': {'m': {'e': {'s': {}}}}, 'o': {'h': {'n': {}}}},
 'o': {'n': {}},
 't': {'e': {'a': {'c': {'h': {'e': {'r': {}}}}}}, 'h': {'e': {}}},
 'w': {'h': {'i': {'l': {'e': {}}}}}}

Another way of looking at this is by looking at the sub-dict for key 'j':

 'j' : 
     {'a': {'m': {'e': {'s': {}}}}, 
      'o': {'h': {'n': {}}}
     }

You can see that both james and john have the same suffix, j, and the rest of the words are different.

List Based Tries

An alternative representation of a trie uses lists. You cannot use chars as indices in a list, but you can use char ordinals. Together with the fact that there are 26 characters (excluding punctuation, and assuming that the characters are not case-sensitive) you can encode each level of a trie in an array with 26 elements. This will use up massively more memory than a dict-based trie, but it can be made to work nonethelesss:

class ListBasedTrie(object):

    # the constructor creates an empty trie
    def __init__(self):
        self.data = [None] * 26

    # add a word by adding each character on different levels
    def Add(self, word):

        # k is the current level
        k = self.data

        # for each character in the word
        for c in word:
        
            # get index of list by mapping ord(c) to 0..26
            o = ord(c) - ord('a')
            assert (o >= 0) and (o < 26)

            # if we have not seen this character on this level before, create a new level
            if k[o] is None:
                k[o] = [None] * 26

            # move to the next level
            k = k[o]

Autocomplete - A Trie Problem

Many text editors, operating system functions, web pages (Google!) have an auto-complete function, where once you type a few characters, it will come up with a list of suggestions for complete words. Think about how you would encode that.

A trie could be a very efficient solution: Once you've matched the first n characters, you automatically get a list of words that also match the same suffix.

Your mission, should you choose to accept it:

The main body of the program is a direct match of these instructions:

#! -*- Encoding: Latin-1 -*-
...
def selftest():
    words = read_words_from_file(filename)
    trie = insert_words_in_trie(words)
    pprint.pprint(trie.AutoCompleteList("impl"))

if __name__ == "__main__":
    selftest()

To read the words from the file, you can just read the file in a string and split it. But since the input document contains punctuation and special characters, you should clean the data before processing it. The following small code will do just that:

def read_words_from_file(filename):
    data = list(open(filename).read())
    
    for index, char in enumerate(data):
        o = ord(char) - ord('a')
        if o < 0 or o >= 26:
            data[index] = ' '
    
    data = "".join(data)
    return data.split()

Inserting the words in the trie is also very simple:

def insert_words_in_trie(words):
    trie = DictBasedTrie()
    for word in words:
        trie.Add(word)
    return trie

That leaves us with two small problems:

  1. Looking up a word in the trie
  2. Creating a list of all words matching the same suffix

The first task is really easy - essentially the same as the insertion code. The second task requires a bit of recursive thinking (and the logic is actually quite close to the one used when generating permutations).

class ListBasedTrie(object):
    ...
    def AutoCompleteList(self, word):
        
        # lookup word in trie - essentially the same as add word to trie, but without the adding part
        k = self.data
        for c in word:
            try:
                k = k[c]
            except KeyError:
                # word is not found in list -> no result
                return []

        # creating the list of words matching the same suffix, recursively
        result = []
        current_word = [None] * 60
        def recurse(k, index):
            if not k:
                result.append( word + "".join(current_word[:index]) )
                
            else:
                for c in k.keys():
                    current_word[index] = c
                    recurse(k[c], index+1)
        recurse(k, 0)
        return result

The results for 'impl' are ('implied', 'implored'), the results for 'imp' are ('impassable', 'impact', 'imparted', 'impatience', 'impatient', 'impetus', 'impersonal', 'imperceptibly', 'imperfect', 'impediment', 'impeded', 'impenetrably', 'impenetrable', 'impinged', 'implied', 'implored', 'impossibility', 'impossible', 'imposed', 'important', 'importunities', 'impotent', 'imprisoned', 'imprisonment', 'impressions', 'impressively', 'impressed', 'improbable', 'impulsively', 'impulse'). Improbably impressive, I should say!