As an interesting exercise in Python 3.3, I implemented a Trie (also known as a Prefix Tree).
Example usages
Create from list of key-value-tuples
mapping = (('she', 1), ('sells', 5), ('sea', 10), ('shells', 19), ('today', 5))
trie = Trie.from_mapping(mapping)
some_prefix = 'sh'
some_key = 'today'
print('Keys:', *trie)
print('Items: ', *trie.items())
print('Keys prefixed "{}":'.format(some_prefix), *trie.keys(prefix=some_prefix))
trie[some_key] = -55
print('Value of "{}":'.format(some_key), trie[some_key])
print(some_key, 'is in' if some_key in trie else 'is not in', 'the Trie')
Output
Keys: today sells sea she shells Items: ('today', 5) ('sells', 5) ('sea', 10) ('she', 1) ('shells', 19) Keys prefixed "sh": she shells Value of "today": -55 today is in the Trie
Find out lengths of words in a string
words = 'She sells sea shells today she sells no shells she sells'.split()
trie = Trie.from_keys(words, value_selector=len)
print(*trie.items())
Output
('sells', 5) ('sea', 3) ('she', 3) ('shells', 6) ('today', 5) ('no', 2) ('She', 3)
Implementation
For simplicity, each entry is represented by a dict
mapping characters to child entries – if this is problematic, feel free to remark on that.
class _Entry:
def __init__(self, value=None):
self.value = value
self.children = {}
def __iter__(self):
return iter(self.children.items())
I made it iterable so I don't have to constantly write things like entry.children.items()
.
Here's the Trie
itself – please feel free to point out any or all issues you can spot. I started taking a look at Python two weeks ago, so I'd also appreciate your comments on how Pythonic (or not) this is, as well as any potential issues of performance.
class Trie:
@staticmethod
def from_mapping(mapping):
"""
Constructs a Trie from an iterable of two-item tuples.
:param mapping: An iterable of key value pairs (each key must itself
be an iterable)
:return: A new Trie containing the keys and values in the mapping
"""
trie = Trie()
for key, value in mapping:
if not key:
continue
trie[key] = value
return trie
@staticmethod
def from_keys(keys, value_selector):
"""
Constructs a Trie from an iterable of keys, mapping each key to the
value returned by the value selector function, which is passed each key.
:param keys: an iterable of keys (each key must itself be an iterable)
:return: a new Trie containing the keys in the mapping
"""
trie = Trie()
for key in keys:
if not key:
continue
trie[key] = value_selector(key)
return trie
def __init__(self):
"""
Creates an empty Trie.
"""
self._root = _Entry()
self._length = 0
def __getitem__(self, key):
"""
Gets the value corresponding to the specified key.
:param key: an iterable
:return: the value corresponding to the key if it is in the Trie,
otherwise None
"""
entry = self._find(key)
if entry is not None:
return entry.value
def __setitem__(self, key, value):
"""
Sets or inserts the specified value, mapping the specified key to it
:param key: an iterable to update or insert
:param value: the object to associate with the key
"""
self.update(key, lambda old_value: value)
def __delitem__(self, key):
"""
Removes the value of the specified key without removing it from the Trie
:param key: the iterable whose value is to be removed
"""
self[key] = None
self._length -= 1
def __contains__(self, key):
"""
Determines if the key is in the Trie and has a value associated with it
:param key: the iterable to search for
:return: True if the Trie contains the key, otherwise False
"""
entry = self._find(key)
return entry is not None and entry.value is not None
def __len__(self):
return self._length
def __bool__(self):
return len(self) > 0
def __iter__(self):
return iter(self.keys())
def keys(self, prefix=''):
"""
Searches for keys beginning with the specified prefix
:param prefix: an iterable
:return: A generator yielding the keys one by one, as they are found
"""
yield from (key for key, value in self.items(prefix))
def items(self, prefix=''):
"""
Searches for key value pairs where the keys begin with the specified
prefix
:param prefix: an iterable
:return: A generator yielding tuples of keys and values as they are
found
"""
start = self._find(prefix)
if start is None:
return ()
stack = [(prefix, start)]
while stack:
current_prefix, entry = stack.pop()
if entry.value is not None:
yield current_prefix, entry.value
for char, children in entry:
stack.append((current_prefix + char, children))
def update(self, key, value_selector):
"""
Updates key with the value obtained from the value selector function,
to which the old value corresponding to the key is passed.
:param key: the key whose value is to be updated
:param value_selector: a function that takes the old value as an
argument and returns a new value
"""
entry = self._root
for char in key:
if char not in entry.children:
entry.children[char] = _Entry()
entry = entry.children[char]
new_value = value_selector(entry.value)
if entry.value != new_value:
entry.value = new_value
self._length += 1
def _find(self, key):
entry = self._root
for char in key:
if char not in entry.children:
return None
entry = entry.children[char]
return entry