A Trie (pronounced “try”), or prefix tree, stores a set of strings by sharing their prefixes. Each edge is labeled by a character; walking from the root and reading labels spells prefixes. We mark nodes where a word ends (a boolean flag, often called isWord
or end
).
a..z
and auto-lowercase user input.end=true
.end=true
.k
words.Operation | Time | Space (per insert) | Notes |
---|---|---|---|
insert / search / startsWith | O(L) | O(L) | L = length of word/prefix |
autocomplete (k results) | O(L + k·avgLen) | — | Traverse prefix then collect |
delete | O(L) | — | Unmark + prune unused nodes |
memory | — | O(total chars) | Prefix sharing saves space; compare to HashSet strings |
char c; left < c < right; eq to advance
). Space-efficient with sorted-array performance characteristics.dict
childrenclass Node: __slots__ = ("end","ch") def __init__(self): self.end=False; self.ch={} class Trie: def __init__(self): self.root=Node() def insert(self, w: str) -> None: u=self.root for c in w: if c not in u.ch: u.ch[c]=Node() u=u.ch[c] u.end=True def search(self, w: str) -> bool: u=self.root for c in w: u=u.ch.get(c); if u is None: return False return u.end def startsWith(self, p: str) -> bool: u=self.root for c in p: u=u.ch.get(c) if u is None: return False return True def delete(self, w: str) -> None: def rec(u,i): if i==len(w): if not u.end: return False, False u.end=False return True, (not u.ch) c=w[i] v=u.ch.get(c) if v is None: return False, False existed, prune = rec(v,i+1) if prune: u.ch.pop(c, None) return existed, (not u.end and not u.ch) rec(self.root,0)
HashMap<Character,Node>
import java.util.*; class Trie { static class Node { boolean end; Map<Character,Node> ch=new HashMap<>(); } private final Node root=new Node(); public void insert(String w){ Node u=root; for(char cc: w.toCharArray()){ char c=(char)('a'+(cc-'a')); // assume cleaned to a..z u.ch.putIfAbsent(c, new Node()); u=u.ch.get(c); } u.end=true; } public boolean search(String w){ Node u=root; for(char c: w.toCharArray()){ u=u.ch.get(c); if(u==null) return false; } return u.end; } public boolean startsWith(String p){ Node u=root; for(char c: p.toCharArray()){ u=u.ch.get(c); if(u==null) return false; } return true; } public void delete(String w){ rec(root,w,0); } private boolean rec(Node u, String w, int i){ if(i==w.length()){ if(!u.end) return false; u.end=false; return u.ch.isEmpty(); } Node v=u.ch.get(w.charAt(i)); if(v==null) return false; boolean prune = rec(v,w,i+1); if(prune) u.ch.remove(w.charAt(i)); return !u.end && u.ch.isEmpty(); } }
unordered_map<char,Node*>
#include <unordered_map> #include <string> using namespace std; struct Node{ bool end=false; unordered_map<char,Node*> ch; }; struct Trie{ Node* root = new Node(); void insert(const string& w){ Node* u=root; for(char c: w){ if(!u->ch.count(c)) u->ch[c]=new Node(); u=u->ch[c]; } u->end=true; } bool search(const string& w){ Node* u=root; for(char c: w){ auto it=u->ch.find(c); if(it==u->ch.end()) return false; u=it->second; } return u->end; } bool startsWith(const string& p){ Node* u=root; for(char c: p){ auto it=u->ch.find(c); if(it==u->ch.end()) return false; u=it->second; } return true; } void erase(const string& w){ rec(root,w,0); } bool rec(Node* u, const string& w, int i){ if(i==(int)w.size()){ if(!u->end) return false; u->end=false; return u->ch.empty(); } auto it=u->ch.find(w[i]); if(it==u->ch.end()) return false; bool prune = rec(it->second,w,i+1); if(prune) u->ch.erase(w[i]); return !u->end && u->ch.empty(); } };
Edges carry strings instead of single chars. During traversal, we match the longest common prefix of the remaining query and the edge label.
// Node has map<char, (label, child)>; label is string segment insert(w): u = root i = 0 while i < len(w): c = w[i] if no edge from u starting with c: add edge (w[i..], new node end=true); return (lab, v) = edge[u][c] k = LCP(lab, w[i..]) if k == len(lab): u = v; i += k; continue // split edge at k mid = new node edge[u][c] = (lab[0..k), mid] edge[mid][lab[k]] = (lab[k..], v) if k == len(w)-i: mid.end = true else edge[mid][w[i+k]] = (w[i+k..], new node end=true) return search(w): u=root; i=0 while i < len(w): c=w[i]; if no edge[u][c]: return false (lab,v)=edge[u][c] if w[i..].startsWith(lab): i+=len(lab); u=v else return false return u.end
Each node stores one character c
and three pointers: lo
for characters < c, eq
to advance to the next character (when equal), and hi
for > c. Words end by setting end=true
at the node that matched the final character.
class TNode: __slots__=("c","lo","eq","hi","end") def __init__(self,c): self.c=c; self.lo=self.eq=self.hi=None; self.end=False class TST: def __init__(self): self.root=None def insert(self, w): def rec(u, i): c=w[i] if u is None: u=TNode(c) if c < u.c: u.lo = rec(u.lo,i) elif c > u.c: u.hi = rec(u.hi,i) else: if i+1==len(w): u.end=True else: u.eq = rec(u.eq,i+1) return u if w: self.root = rec(self.root,0) def search(self, w): u=self.root; i=0 while u: c=w[i] if c < u.c: u=u.lo elif c > u.c: u=u.hi else: if i+1==len(w): return u.end i+=1; u=u.eq return False
class TST { static class Node { char c; Node lo,eq,hi; boolean end; Node(char c){this.c=c;} } Node root; public void insert(String w){ root = ins(root,w,0); } private Node ins(Node u, String w, int i){ char c=w.charAt(i); if(u==null) u=new Node(c); if(c<u.c) u.lo=ins(u.lo,w,i); else if(c>u.c) u.hi=ins(u.hi,w,i); else if(i+1==w.length()) u.end=true; else u.eq=ins(u.eq,w,i+1); return u; } public boolean search(String w){ Node u=root; int i=0; while(u!=null){ char c=w.charAt(i); if(c<u.c) u=u.lo; else if(c>u.c) u=u.hi; else { if(i+1==w.length()) return u.end; i++; u=u.eq; } } return false; } }
struct TNode{ char c; bool end=false; TNode* lo=nullptr; TNode* eq=nullptr; TNode* hi=nullptr; TNode(char c):c(c){} }; TNode* ins(TNode* u, const string& w, int i){ char c=w[i]; if(!u) u=new TNode(c); if(c<u->c) u->lo=ins(u->lo,w,i); else if(c>u->c) u->hi=ins(u->hi,w,i); else if(i+1==(int)w.size()) u->end=true; else u->eq=ins(u->eq,w,i+1); return u; } bool search(TNode* u, const string& w){ int i=0; while(u){ char c=w[i]; if(c<u->c) u=u->lo; else if(c>u->c) u=u->hi; else { if(i+1==(int)w.size()) return u->end; i++; u=u->eq; } } return false; }
set
of words + bisect
over a sorted list can do prefix ranges (O(log n + k)
), but Tries win when many prefixes or character-by-character traversal is needed.TreeSet
supports subSet(prefixRange)
to emulate prefix queries; HashSet
for exact words.std::set
/std::map
support prefix range via lower_bound
/upper_bound
.O(L)
regardless of the number of words.O(L + k)
behavior when results are limited to k
.