Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A trie makes autocomplete predictable: store each string as a path of character nodes, share the nodes for common prefixes, then explore the subtree below the prefix a user typed. Insertion, exact lookup, and prefix lookup each walk at most L characters for an input of length L; producing suggestions additionally costs whatever traversal and output the suggestions require.

This guide builds a working trie in Python, including exact search, prefix checks, bounded autocomplete, deletion, normalization choices, complexity, memory trade-offs, and tests that expose the most common mistakes.

What a trie stores

The root represents the empty prefix. Every outgoing edge carries one character, and every node therefore represents the prefix reached from the root. A boolean terminal marker, commonly named is_word, records whether that prefix is a complete stored key.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
root
 └─ c
    └─ a
       ├─ r  (word: true)
       └─ t  (word: true)
 └─ d
    └─ o
       └─ g  (word: true)

The paths for car and cat share the c and a nodes. The marker matters: if only car is stored, the node for ca still exists, but ca is not an exact key. A search must inspect the marker after consuming the final character; a prefix query only needs the path to exist.

Choose the input contract before coding

Alphabet and case

Decide whether keys are lowercase ASCII, unrestricted Unicode, or another defined alphabet. A fixed child array can be compact and fast for a tiny known alphabet, but a mapping is safer for sparse or broad character sets. The example below uses a dictionary and applies Unicode casefold(), so Cat and cat address the same key. If your product must preserve case, replace that normalization with an identity function.

Normalization

Case folding is not the same as every form of Unicode normalization. If visually equivalent sequences must compare identically, normalize them consistently on insertion and every query, for example with a chosen Unicode normalization form before case folding. Document the policy because changing it later can make existing keys unreachable.

Empty strings and duplicates

This implementation rejects an empty key to keep the root reserved for the empty prefix. Inserting an existing key is idempotent: it sets the terminal marker again and creates no duplicate path. If your API needs an empty string as a valid key, allow the root marker and define how autocomplete of an empty prefix is capped.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Implement the trie in Python

Each node has a child mapping and one terminal flag. The public methods all normalize their input through the same helper, preventing an insertion/search mismatch.

class TrieNode:
    __slots__ = ('children', 'is_word')

    def __init__(self):
        self.children = {}
        self.is_word = False


class Trie:
    def __init__(self):
        self.root = TrieNode()

    def _normalize(self, text):
        return text.casefold()

    def _walk(self, text):
        node = self.root
        for ch in text:
            node = node.children.get(ch)
            if node is None:
                return None
        return node

    def insert(self, word):
        word = self._normalize(word)
        if not word:
            raise ValueError('empty keys are not supported')
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_word = True

    def search(self, word):
        word = self._normalize(word)
        node = self._walk(word)
        return node is not None and node.is_word

    def starts_with(self, prefix):
        prefix = self._normalize(prefix)
        return self._walk(prefix) is not None

    def autocomplete(self, prefix, limit=10):
        if limit <= 0:
            return []
        prefix = self._normalize(prefix)
        node = self._walk(prefix)
        if node is None:
            return []

        results = []

        def visit(current, text):
            if len(results) >= limit:
                return
            if current.is_word:
                results.append(text)
            for ch in sorted(current.children):
                visit(current.children[ch], text + ch)
                if len(results) >= limit:
                    return

        visit(node, prefix)
        return results

    def delete(self, word):
        word = self._normalize(word)
        if not word:
            return False
        node = self.root
        path = []
        for ch in word:
            child = node.children.get(ch)
            if child is None:
                return False
            path.append((node, ch, child))
            node = child
        if not node.is_word:
            return False
        node.is_word = False

        for parent, ch, child in reversed(path):
            if child.is_word or child.children:
                break
            del parent.children[ch]
        return True

Insertion

insert starts at the root and creates a missing child for each character. Only the final node is marked as a word. Thus inserting car after cat reuses the existing c and a nodes and adds one branch.

Exact search

search follows the complete key. A missing child immediately returns False. Reaching a node is not enough: the terminal flag must be true. With only car stored, search('ca') correctly returns false.

Prefix lookup

starts_with returns true as soon as every prefix character has a corresponding edge. It deliberately ignores is_word, because a prefix can be valid even when it is not itself stored as a key.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Autocomplete in two stages

The method first locates the node for the requested prefix. It then performs a depth-first traversal of descendants, emitting the prefix itself when that node is terminal and appending each continuation. Sorting child characters gives deterministic lexicographic output; a ranked product should replace that policy with explicit scores and a stable tie-break rule.

The limit guard stops traversal once enough results have been emitted. Without a cap, a short prefix such as a can visit a very large subtree and return an impractical response.

Deletion and pruning

Deletion first verifies that the complete key exists, clears its terminal marker, and walks the recorded path upward. A node is removed only when it has no children and is not itself another word. Shared nodes therefore survive: deleting car does not damage cat.

Run a small trace

  1. Create a Trie and insert car, cat, dog, and door.
  2. search('car') returns true, while search('ca') returns false.
  3. starts_with('ca') returns true, and starts_with('cz') returns false.
  4. autocomplete('ca', 5) returns ['car', 'cat'] under the sorted-child policy.
  5. After delete('car'), search('car') is false but search('cat') remains true and the ca branch remains intact.

Complexity: what scales with key length

Let L be the number of characters in the input key or prefix. With dictionary-like child lookup that is expected constant time, the character walk gives the following costs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Operation Time What the bound assumes
Insert O(L) One child lookup or creation per character.
Exact search O(L) One child lookup per character, then one terminal check.
Prefix lookup O(L) The walk stops at the first missing edge.
Autocomplete O(L + V + R) V is visited subtree work and R is the characters emitted in returned results; a limit can reduce both.
Delete O(L) The path is walked once and pruned upward at most once per character.

The O(L) statements are not promises about every implementation. A sorted child structure, linear child list, or unusual runtime changes the cost of finding a child. Autocomplete also cannot be independent of its output: returning many long suggestions necessarily requires work to visit and construct them.

Trie versus simpler structures

Structure Exact membership Prefix queries Memory and update profile
Unsorted list Linear scan through stored keys. Usually scans every key and tests its beginning. Small structural overhead; simple inserts, but query time grows with the collection.
Hash set Expected constant-time membership under normal hash-table assumptions. No direct prefix operation; finding matches generally requires scanning all keys or maintaining another index. Often the simpler, leaner choice when exact membership is the only requirement.
Plain trie Character walk in O(L). Direct prefix walk followed by subtree traversal and an output policy. More node and child-map overhead, offset by shared prefixes; supports incremental insertion and pruning.
Radix or compressed trie Similar prefix-oriented behavior, with comparisons over stored substrings. Fewer nodes on long single-child chains. Reduces overhead but makes splitting and merging edge labels more involved.

Choose a trie when prefix lookup or completion is a primary operation and the saved prefix work justifies its memory. Choose a hash set for a large exact-membership workload with no need to enumerate related keys. A compressed trie is worth considering when ordinary trie nodes consume too much memory because many paths have little branching.

Memory and child-representation trade-offs

A node-per-character design can use substantial memory. Every node carries object overhead, a child mapping, and references, and a broad alphabet can leave many possible characters unused. Prefix sharing helps most when keys have common beginnings; unrelated random strings share little and approach one node per character.

For a known small alphabet, an array indexed by character can avoid per-edge hash overhead and provide predictable access, but it allocates slots for absent children and requires a mapping from characters to indexes. Dictionaries handle sparse children and Unicode naturally at the cost of per-entry overhead. Measure with your actual key distribution before changing representations.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Make autocomplete useful, not merely correct

Set an explicit result policy

Define a maximum number of suggestions, whether the prefix itself appears when it is a complete key, and the ordering rule. Lexicographic order is reproducible and easy to test. Frequency, recency, language, or personalized ranking requires storing metadata and a ranking stage; the trie supplies candidates but does not decide relevance by itself.

Best Value
Sale
Cracking the Coding Interview: 189 Programming Questions and Solutions
  • Careercup, Easy To Read
  • Condition : Good
  • Compact for travelling

Control worst-case work

Cap results at the API boundary, reject unreasonably long inputs, and avoid traversing the entire tree when the caller needs only a handful of candidates. If ranking requires examining more candidates than are returned, document that broader candidate budget separately from the response limit.

Keep updates consistent

Apply the same normalization to bulk loading, live insertion, deletion, and queries. If keys come from multiple languages, test combining marks, case-folding edge cases, and characters outside the original alphabet. Persisting normalized keys while displaying an original spelling requires storing display text as separate payload data.

Tests and failure modes

  • Prefix mistaken for a word: insert only car; assert that search('ca') is false and starts_with('ca') is true.
  • Shared branch removed: insert car and cat, delete car, then verify cat and autocomplete for ca.
  • Missing path: query a prefix that diverges at the first character and one that diverges near the end; both should return quickly and cleanly.
  • Duplicate insertion: insert the same key repeatedly and verify that results contain it once.
  • Normalization mismatch: insert mixed-case text and query a differently cased form; both should agree under the selected policy.
  • Limit handling: request zero, a negative limit, one result, and a limit larger than the subtree; confirm no extra traversal results leak out.
  • Ordering: insert children in different orders and verify that sorted traversal still returns the documented order.
  • Deletion of a missing key: ensure the method reports false and leaves every existing key untouched.

When a trie is the right tool

Use this structure for dictionaries, command palettes, route names, usernames, or any feature whose central question is which stored keys begin with a supplied prefix. Its operations are easy to reason about because they follow characters rather than scanning unrelated records.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Do not choose a plain trie solely because autocomplete sounds fast. If the application only asks whether an entire key exists, a hash set is usually simpler and more memory-efficient. If memory is the limiting factor and keys contain long unbranched runs, a compressed trie can preserve prefix behavior with fewer nodes. In every case, specify the alphabet, normalization, result cap, and ordering policy alongside the data structure so callers know exactly what a query means.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.