Skip to content

Tries

A Trie trades memory for prefix work. Each edge consumes one character, so lookup time depends on key length rather than the number of stored words.

Ownership model

flowchart LR
  accTitle: Shared prefix ownership in a Trie
  accDescr: Cat and car share root, c, and a nodes, then branch to t and r terminal nodes. The terminal flag distinguishes a complete word from a prefix.
  R["root"] --> C["c"]
  C --> A["a"]
  A --> T["t · terminal"]
  A --> RR["r · terminal"]

Invariant: following the characters of a stored word reaches exactly one path, and only a terminal flag marks a complete word.

Tested template

@dataclass
class _TrieNode:
    children: dict[str, _TrieNode] = field(default_factory=dict)
    terminal: bool = False


class Trie:
    """Store strings by shared prefixes."""

    def __init__(self) -> None:
        self.root = _TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for character in word:
            # Create only the missing suffix; existing prefixes are shared.
            node = node.children.setdefault(character, _TrieNode())
        node.terminal = True

    def contains(self, word: str) -> bool:
        node = self._walk(word)
        return node is not None and node.terminal

    def starts_with(self, prefix: str) -> bool:
        return self._walk(prefix) is not None

    def _walk(self, text: str) -> _TrieNode | None:
        node = self.root
        for character in text:
            node = node.children.get(character)
            if node is None:
                return None
        return node
class Trie {
 public:
  Trie() : nodes_(1) {}

  void insert(const std::string& word) {
    int node = 0;
    for (const char character : word) {
      const auto found = nodes_[node].children.find(character);
      if (found == nodes_[node].children.end()) {
        // Create only the missing suffix; existing prefixes are shared.
        const int child = static_cast<int>(nodes_.size());
        nodes_[node].children[character] = child;
        nodes_.push_back(Node());
        node = child;
      } else {
        node = found->second;
      }
    }
    nodes_[node].terminal = true;
  }

  bool contains(const std::string& word) const {
    const int node = walk(word);
    return node >= 0 && nodes_[node].terminal;
  }

  bool starts_with(const std::string& prefix) const {
    return walk(prefix) >= 0;
  }

 private:
  struct Node {
    std::unordered_map<char, int> children;
    bool terminal = false;
  };

  int walk(const std::string& text) const {
    int node = 0;
    for (const char character : text) {
      const auto found = nodes_[node].children.find(character);
      if (found == nodes_[node].children.end()) {
        return -1;
      }
      node = found->second;
    }
    return node;
  }

  std::vector<Node> nodes_;
};
class Trie {
 public:
  Trie() : nodes_(1) {}

  void insert(const std::string& word) {
    int node = 0;
    for (const char character : word) {
      const auto found = nodes_[node].children.find(character);
      if (found == nodes_[node].children.end()) {
        // Create only the missing suffix; existing prefixes are shared.
        const int child = static_cast<int>(nodes_.size());
        nodes_[node].children[character] = child;
        nodes_.push_back(Node());
        node = child;
      } else {
        node = found->second;
      }
    }
    nodes_[node].terminal = true;
  }

  bool contains(const std::string& word) const {
    const int node = walk(word);
    return node >= 0 && nodes_[node].terminal;
  }

  bool starts_with(const std::string& prefix) const {
    return walk(prefix) >= 0;
  }

 private:
  struct Node {
    std::unordered_map<char, int> children;
    bool terminal = false;
  };

  int walk(const std::string& text) const {
    int node = 0;
    for (const char character : text) {
      const auto found = nodes_[node].children.find(character);
      if (found == nodes_[node].children.end()) {
        return -1;
      }
      node = found->second;
    }
    return node;
  }

  std::vector<Node> nodes_;
};

Insert and lookup take O(length) time. Space is proportional to created prefix nodes; dense alphabets may need compressed edges or sparse maps.

Extensions

  • Store a count at each node for prefix-frequency queries.
  • Store top suggestions for autocomplete.
  • Add failure links to form Aho–Corasick for many patterns in one text.
  • Use a bitwise Trie for maximum-XOR queries over integers.

Practice ladder

  1. Implement Trie
  2. Design Add and Search Words
  3. Replace Words
  4. Word Search II