Tries
A tree of characters where every path from the root spells a prefix, so word and prefix lookups cost O(length of the word).
How It Works
A trie (also called a prefix tree, and usually pronounced "try") stores strings one character per level. The root stands for the empty string and each edge adds one letter, so every node stands for the prefix spelled on the path down to it. Words that start the same way share the same path, and a flag on each node records whether a whole word ends there.
words: cat, car, card, dog
(root)
/ \
c d
| |
a o
/ \ |
t* r* g*
|
d*
* marks a node where a word endsInserting or looking up a word walks one node per character, so it costs O(L) for a word of length L, however many words the trie holds. A hash set can also check a whole word in O(L), but it can't answer does any word start with this prefix? without looking at every word. A trie walks the prefix, and the words below the node where it stops are exactly the words with that prefix.
Building and Searching a Trie
Step 1 of 15
root
- Node just created
- Path being followed
- Lookup succeeds here
- Lookup fails here
An empty trie is just the root, which stands for the empty string "". Each edge adds one letter, so every node stands for the prefix spelled on the way down to it.
All 15 steps as text
- An empty trie is just the root, which stands for the empty string
"". Each edge adds one letter, so every node stands for the prefix spelled on the way down to it. - Insert
cat. The root has nocchild, so create the node forc. chas noachild, so create the node forca.cahas notchild, so create the node forcat. That's the last letter, so mark it (the second ring): a whole word ends here.- Insert
car. It sharescawith the words already stored, so follow those nodes instead of creating new ones. cahas norchild, so create the node forcar. Mark it, becausecarends here.- Insert
card. It sharescarwith the words already stored, so follow those nodes instead of creating new ones. carhas nodchild, so create the node forcard. Mark it, becausecardends here.- Insert
dog. The root has nodchild, so create the node ford. dhas noochild, so create the node fordo.dohas nogchild, so create the node fordog. Mark it, becausedogends here.search("car")followsc,aandrto the node forcar. It's marked, so a whole word ends there andsearchreturnstrue.search("ca")followscandato the node forca, but that node isn't marked:cais only a prefix ofcar,cardandcat, sosearchreturnsfalse.startsWith("ca")followscandato the node forca. Any path at all means some word continues it, so it returnstrue:car,cardandcatall sit below.search("cow"):chas noochild, so the walk falls off the trie. No stored word starts withco, so it returnsfalsewithout looking further.
Each node needs a way to find its children. A fixed array of 26 slots, one per lowercase letter, is fast and simple: the child for ch lives at index ch - 'a'. A hash map from character to child saves memory when nodes have few children and works for any alphabet. The examples here use the 26-slot array, because every input is lowercase English letters.
- Prefix queries. Autocomplete,
startsWith, or counting the words under a prefix. - Pattern search. A wildcard such as
.turns the walk into a depth-first search over every branch that could match. - Prefix matching along a word. Walk a word through the trie and stop at the first (or last) node where a stored word ends, as in Replace Words.
- Many words at once. Build a trie of the dictionary, then search a grid or a stream for all of the words in one pass, as in Word Search II.
When to Reach for It
- The problem is about prefixes: does any word start with this, or what's the shortest root of this word?
- You check many strings against the same dictionary, one character at a time.
- The feature is autocomplete, spell check or search as you type.
- A wildcard such as
.can stand for any single letter. - You search a grid or a stream for any of many dictionary words at once.
Complexity
| Operation | Time | Space |
|---|---|---|
| Insert a word of length L | O(L) | O(L) new nodes at most |
| Search or startsWith | O(L) | O(1) |
| Wildcard search | O(L) without dots, up to O(N) with them | O(L) recursion |
| Store words with N characters in total | O(N) | O(N) nodes, 26 slots each |
The Template
A Prefix Tree
Every operation walks down from the root one character at a time. insert creates any missing nodes along the way and marks the last one as the end of a word. search and startsWith share a walk helper that follows a string's path and returns the node where it ends, or null if the path runs out first.
The two lookups differ only in the end-of-word flag. After inserting "apple", the path for "app" exists, so startsWith("app") is true. But search("app") stays false until "app" is inserted too. That insert creates no new nodes. It only sets the flag on the second p.
Time O(L) per callSpace O(L) per insert
class TrieNode:
def __init__(self) -> None:
# One slot per lowercase letter. None means no stored word continues that way.
self.children: list[TrieNode | None] = [None] * 26
self.is_word = False # Whether a stored word ends exactly here.
class Trie:
"""A prefix tree over lowercase words."""
def __init__(self) -> None:
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
i = ord(ch) - ord("a")
# Follow the shared prefix. Only create nodes where this word branches off.
if node.children[i] is None:
node.children[i] = TrieNode()
node = node.children[i]
node.is_word = True
def search(self, word: str) -> bool:
# The path has to exist and end on a node marked as a word.
node = self._walk(word)
return node is not None and node.is_word
def starts_with(self, prefix: str) -> bool:
# Any path at all means some stored word continues it.
return self._walk(prefix) is not None
def _walk(self, prefix: str) -> TrieNode | None:
"""The node at the end of `prefix`, or None if no stored word starts with it."""
node = self.root
for ch in prefix:
node = node.children[ord(ch) - ord("a")]
if node is None:
return None # Fell off the trie: nothing continues this way.
return node
if __name__ == "__main__":
trie = Trie()
trie.insert("apple")
print(trie.search("apple")) # True
print(trie.search("app")) # False
print(trie.starts_with("app")) # True
trie.insert("app")
print(trie.search("app")) # True
print(trie.starts_with("apx")) # Falseclass TrieNode {
// One slot per lowercase letter. null means no stored word continues that way.
children: (TrieNode | null)[] = new Array<TrieNode | null>(26).fill(null);
isWord = false; // Whether a stored word ends exactly here.
}
// A prefix tree over lowercase words.
export class Trie {
root = new TrieNode();
insert(word: string): void {
let node = this.root;
for (const ch of word) {
const i = ch.charCodeAt(0) - "a".charCodeAt(0);
// Follow the shared prefix. Only create nodes where this word branches off.
node = node.children[i] ??= new TrieNode();
}
node.isWord = true;
}
search(word: string): boolean {
// The path has to exist and end on a node marked as a word.
const node = this.walk(word);
return node !== null && node.isWord;
}
startsWith(prefix: string): boolean {
// Any path at all means some stored word continues it.
return this.walk(prefix) !== null;
}
// The node at the end of `prefix`, or null if no stored word starts with it.
private walk(prefix: string): TrieNode | null {
let node: TrieNode | null = this.root;
for (const ch of prefix) {
node = node.children[ch.charCodeAt(0) - "a".charCodeAt(0)];
if (node === null) return null; // Fell off the trie: nothing continues this way.
}
return node;
}
}
const trie = new Trie();
trie.insert("apple");
console.log(trie.search("apple")); // true
console.log(trie.search("app")); // false
console.log(trie.startsWith("app")); // true
trie.insert("app");
console.log(trie.search("app")); // true
console.log(trie.startsWith("apx")); // falsepackage main
import "fmt"
// TrieNode is one node of a trie.
type TrieNode struct {
// One slot per lowercase letter. nil means no stored word continues that way.
children [26]*TrieNode
isWord bool // Whether a stored word ends exactly here.
}
// Trie is a prefix tree over lowercase words.
type Trie struct {
root *TrieNode
}
func newTrie() *Trie {
return &Trie{root: &TrieNode{}}
}
func (t *Trie) insert(word string) {
node := t.root
for _, ch := range word {
i := ch - 'a'
// Follow the shared prefix. Only create nodes where this word branches off.
if node.children[i] == nil {
node.children[i] = &TrieNode{}
}
node = node.children[i]
}
node.isWord = true
}
func (t *Trie) search(word string) bool {
// The path has to exist and end on a node marked as a word.
node := t.walk(word)
return node != nil && node.isWord
}
func (t *Trie) startsWith(prefix string) bool {
// Any path at all means some stored word continues it.
return t.walk(prefix) != nil
}
// walk returns the node at the end of prefix, or nil if no stored word starts
// with it.
func (t *Trie) walk(prefix string) *TrieNode {
node := t.root
for _, ch := range prefix {
node = node.children[ch-'a']
if node == nil {
return nil // Fell off the trie: nothing continues this way.
}
}
return node
}
func main() {
trie := newTrie()
trie.insert("apple")
fmt.Println(trie.search("apple")) // true
fmt.Println(trie.search("app")) // false
fmt.Println(trie.startsWith("app")) // true
trie.insert("app")
fmt.Println(trie.search("app")) // true
fmt.Println(trie.startsWith("apx")) // false
}using System;
public class Solution
{
public class TrieNode
{
// One slot per lowercase letter. null means no stored word continues that way.
public TrieNode?[] Children = new TrieNode?[26];
public bool IsWord; // Whether a stored word ends exactly here.
}
// A prefix tree over lowercase words.
public class Trie
{
readonly TrieNode root = new TrieNode();
public void Insert(string word)
{
var node = root;
foreach (char ch in word)
{
int i = ch - 'a';
// Follow the shared prefix. Only create nodes where this word branches off.
node = node.Children[i] ??= new TrieNode();
}
node.IsWord = true;
}
public bool Search(string word)
{
// The path has to exist and end on a node marked as a word.
var node = Walk(word);
return node != null && node.IsWord;
}
public bool StartsWith(string prefix)
{
// Any path at all means some stored word continues it.
return Walk(prefix) != null;
}
// The node at the end of prefix, or null if no stored word starts with it.
TrieNode? Walk(string prefix)
{
TrieNode? node = root;
foreach (char ch in prefix)
{
node = node.Children[ch - 'a'];
if (node == null) return null; // Fell off the trie: nothing continues this way.
}
return node;
}
}
public static void Main()
{
var trie = new Trie();
trie.Insert("apple");
Console.WriteLine(trie.Search("apple")); // True
Console.WriteLine(trie.Search("app")); // False
Console.WriteLine(trie.StartsWith("app")); // True
trie.Insert("app");
Console.WriteLine(trie.Search("app")); // True
Console.WriteLine(trie.StartsWith("apx")); // False
}
}public class Solution {
static class TrieNode {
// One slot per lowercase letter. null means no stored word continues that way.
TrieNode[] children = new TrieNode[26];
boolean isWord; // Whether a stored word ends exactly here.
}
// A prefix tree over lowercase words.
static class Trie {
private final TrieNode root = new TrieNode();
void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int i = ch - 'a';
// Follow the shared prefix. Only create nodes where this word branches off.
if (node.children[i] == null) node.children[i] = new TrieNode();
node = node.children[i];
}
node.isWord = true;
}
boolean search(String word) {
// The path has to exist and end on a node marked as a word.
TrieNode node = walk(word);
return node != null && node.isWord;
}
boolean startsWith(String prefix) {
// Any path at all means some stored word continues it.
return walk(prefix) != null;
}
// The node at the end of prefix, or null if no stored word starts with it.
private TrieNode walk(String prefix) {
TrieNode node = root;
for (char ch : prefix.toCharArray()) {
node = node.children[ch - 'a'];
if (node == null) return null; // Fell off the trie: nothing continues this way.
}
return node;
}
}
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("apple");
System.out.println(trie.search("apple")); // true
System.out.println(trie.search("app")); // false
System.out.println(trie.startsWith("app")); // true
trie.insert("app");
System.out.println(trie.search("app")); // true
System.out.println(trie.startsWith("apx")); // false
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
Design a word dictionary with two operations. addWord(word) stores a word. search(pattern) returns whether any stored word matches the pattern, where each . matches any one letter and every other character must match exactly. Words and patterns use lowercase letters.
Example: after adding "bad", "dad" and "mad", searching for "pad" returns false, while "bad", ".ad" and "b.." all return true.
Show SolutionHide Solution
Approach
addWord is the trie's insert. In search, letters work just as before: follow the one matching child, or fail if it's missing. A dot is where it branches. It could be any letter, so try every child that exists and succeed if any of them leads to a match. That turns the lookup into a depth-first search over the trie, steered by the pattern.
When the pattern runs out, the match only counts if a word ends at that node. That's why "b." fails even though "bad" is stored: the node for ba exists, but no word ends there.
A pattern with no dots costs O(L), like a normal lookup. Each dot can fan out to 26 branches, so dots make it slower, but there's a ceiling. A node sits at exactly one depth, and the search only reaches it at the matching position in the pattern, so one search visits each node at most once: O(N) for a trie with N nodes.
Time O(L) to add, up to O(N) to searchSpace O(L) recursion
class TrieNode:
def __init__(self) -> None:
# One slot per lowercase letter. None means no stored word continues that way.
self.children: list[TrieNode | None] = [None] * 26
self.is_word = False # Whether a stored word ends exactly here.
class WordDictionary:
"""Stores words and finds them by pattern, where "." matches any one letter."""
def __init__(self) -> None:
self.root = TrieNode()
def add_word(self, word: str) -> None:
node = self.root
for ch in word:
i = ord(ch) - ord("a")
if node.children[i] is None:
node.children[i] = TrieNode()
node = node.children[i]
node.is_word = True
def search(self, pattern: str) -> bool:
return self._match(self.root, pattern, 0)
def _match(self, node: TrieNode, pattern: str, i: int) -> bool:
"""Whether some stored word below `node` matches `pattern` from index `i` on."""
# The pattern is used up, so it only matches if a word ends right here.
if i == len(pattern):
return node.is_word
if pattern[i] == ".":
# A dot can be any letter, so try every branch that exists.
for branch in node.children:
if branch is not None and self._match(branch, pattern, i + 1):
return True
return False
child = node.children[ord(pattern[i]) - ord("a")]
return child is not None and self._match(child, pattern, i + 1)
if __name__ == "__main__":
dictionary = WordDictionary()
for word in ["bad", "dad", "mad"]:
dictionary.add_word(word)
print(dictionary.search("pad")) # False
print(dictionary.search("bad")) # True
print(dictionary.search(".ad")) # True
print(dictionary.search("b..")) # True
print(dictionary.search("b.")) # Falseclass TrieNode {
// One slot per lowercase letter. null means no stored word continues that way.
children: (TrieNode | null)[] = new Array<TrieNode | null>(26).fill(null);
isWord = false; // Whether a stored word ends exactly here.
}
// Stores words and finds them by pattern, where "." matches any one letter.
export class WordDictionary {
root = new TrieNode();
addWord(word: string): void {
let node = this.root;
for (const ch of word) {
const i = ch.charCodeAt(0) - "a".charCodeAt(0);
node = node.children[i] ??= new TrieNode();
}
node.isWord = true;
}
search(pattern: string): boolean {
return this.match(this.root, pattern, 0);
}
// Whether some stored word below `node` matches `pattern` from index `i` on.
private match(node: TrieNode, pattern: string, i: number): boolean {
// The pattern is used up, so it only matches if a word ends right here.
if (i === pattern.length) return node.isWord;
if (pattern[i] === ".") {
// A dot can be any letter, so try every branch that exists.
for (const branch of node.children) {
if (branch !== null && this.match(branch, pattern, i + 1)) return true;
}
return false;
}
const child = node.children[pattern.charCodeAt(i) - "a".charCodeAt(0)];
return child !== null && this.match(child, pattern, i + 1);
}
}
const dictionary = new WordDictionary();
for (const word of ["bad", "dad", "mad"]) dictionary.addWord(word);
console.log(dictionary.search("pad")); // false
console.log(dictionary.search("bad")); // true
console.log(dictionary.search(".ad")); // true
console.log(dictionary.search("b..")); // true
console.log(dictionary.search("b.")); // falsepackage main
import "fmt"
// TrieNode is one node of a trie.
type TrieNode struct {
// One slot per lowercase letter. nil means no stored word continues that way.
children [26]*TrieNode
isWord bool // Whether a stored word ends exactly here.
}
// WordDictionary stores words and finds them by pattern, where "." matches
// any one letter.
type WordDictionary struct {
root *TrieNode
}
func newWordDictionary() *WordDictionary {
return &WordDictionary{root: &TrieNode{}}
}
func (d *WordDictionary) addWord(word string) {
node := d.root
for _, ch := range word {
i := ch - 'a'
if node.children[i] == nil {
node.children[i] = &TrieNode{}
}
node = node.children[i]
}
node.isWord = true
}
func (d *WordDictionary) search(pattern string) bool {
return d.match(d.root, pattern, 0)
}
// match reports whether some stored word below node matches pattern from
// index i on.
func (d *WordDictionary) match(node *TrieNode, pattern string, i int) bool {
// The pattern is used up, so it only matches if a word ends right here.
if i == len(pattern) {
return node.isWord
}
if pattern[i] == '.' {
// A dot can be any letter, so try every branch that exists.
for _, branch := range node.children {
if branch != nil && d.match(branch, pattern, i+1) {
return true
}
}
return false
}
child := node.children[pattern[i]-'a']
return child != nil && d.match(child, pattern, i+1)
}
func main() {
dictionary := newWordDictionary()
for _, word := range []string{"bad", "dad", "mad"} {
dictionary.addWord(word)
}
fmt.Println(dictionary.search("pad")) // false
fmt.Println(dictionary.search("bad")) // true
fmt.Println(dictionary.search(".ad")) // true
fmt.Println(dictionary.search("b..")) // true
fmt.Println(dictionary.search("b.")) // false
}using System;
public class Solution
{
public class TrieNode
{
// One slot per lowercase letter. null means no stored word continues that way.
public TrieNode?[] Children = new TrieNode?[26];
public bool IsWord; // Whether a stored word ends exactly here.
}
// Stores words and finds them by pattern, where "." matches any one letter.
public class WordDictionary
{
readonly TrieNode root = new TrieNode();
public void AddWord(string word)
{
var node = root;
foreach (char ch in word)
{
int i = ch - 'a';
node = node.Children[i] ??= new TrieNode();
}
node.IsWord = true;
}
public bool Search(string pattern) => Match(root, pattern, 0);
// Whether some stored word below node matches pattern from index i on.
bool Match(TrieNode node, string pattern, int i)
{
// The pattern is used up, so it only matches if a word ends right here.
if (i == pattern.Length) return node.IsWord;
if (pattern[i] == '.')
{
// A dot can be any letter, so try every branch that exists.
foreach (var branch in node.Children)
{
if (branch != null && Match(branch, pattern, i + 1)) return true;
}
return false;
}
var child = node.Children[pattern[i] - 'a'];
return child != null && Match(child, pattern, i + 1);
}
}
public static void Main()
{
var dictionary = new WordDictionary();
foreach (var word in new[] { "bad", "dad", "mad" }) dictionary.AddWord(word);
Console.WriteLine(dictionary.Search("pad")); // False
Console.WriteLine(dictionary.Search("bad")); // True
Console.WriteLine(dictionary.Search(".ad")); // True
Console.WriteLine(dictionary.Search("b..")); // True
Console.WriteLine(dictionary.Search("b.")); // False
}
}public class Solution {
static class TrieNode {
// One slot per lowercase letter. null means no stored word continues that way.
TrieNode[] children = new TrieNode[26];
boolean isWord; // Whether a stored word ends exactly here.
}
// Stores words and finds them by pattern, where "." matches any one letter.
static class WordDictionary {
private final TrieNode root = new TrieNode();
void addWord(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int i = ch - 'a';
if (node.children[i] == null) node.children[i] = new TrieNode();
node = node.children[i];
}
node.isWord = true;
}
boolean search(String pattern) {
return match(root, pattern, 0);
}
// Whether some stored word below node matches pattern from index i on.
private boolean match(TrieNode node, String pattern, int i) {
// The pattern is used up, so it only matches if a word ends right here.
if (i == pattern.length()) return node.isWord;
if (pattern.charAt(i) == '.') {
// A dot can be any letter, so try every branch that exists.
for (TrieNode branch : node.children) {
if (branch != null && match(branch, pattern, i + 1)) return true;
}
return false;
}
TrieNode child = node.children[pattern.charAt(i) - 'a'];
return child != null && match(child, pattern, i + 1);
}
}
public static void main(String[] args) {
WordDictionary dictionary = new WordDictionary();
for (String word : new String[] {"bad", "dad", "mad"}) dictionary.addWord(word);
System.out.println(dictionary.search("pad")); // false
System.out.println(dictionary.search("bad")); // true
System.out.println(dictionary.search(".ad")); // true
System.out.println(dictionary.search("b..")); // true
System.out.println(dictionary.search("b.")); // false
}
}Problem
You're given a list of roots and a sentence of lowercase words separated by single spaces. Replace every word that starts with a root by that root. If several roots match, use the shortest. Words that don't start with any root stay as they are.
Example: with the roots ["cat", "bat", "rat"], the sentence "the cattle was rattled by the battery" becomes "the cat was rat by the bat".
Show SolutionHide Solution
Approach
Put every root in a trie. For each word, walk the trie along its letters and stop at the first node where a root ends. That's the shortest matching root, because the walk meets the word's prefixes in order of length. If the walk falls off the trie first, no root is a prefix of the word, so it stays.
Without a trie, you'd test every prefix of every word against a hash set of roots. Building and hashing a prefix costs up to O(L), so a word of length L costs O(L²). The trie walk checks all of a word's prefixes in one O(L) pass.
With D the total length of the roots and S the length of the sentence, building the trie takes O(D) and the walks take O(S). The trie holds at most D nodes, and splitting the sentence into words and joining the result take O(S) space.
Time O(D + S)Space O(D + S)
class TrieNode:
def __init__(self) -> None:
# One slot per lowercase letter. None means no stored word continues that way.
self.children: list[TrieNode | None] = [None] * 26
self.is_word = False # Whether a stored root ends exactly here.
def insert(trie: TrieNode, word: str) -> None:
"""Adds `word` to the trie rooted at `trie`."""
node = trie
for ch in word:
i = ord(ch) - ord("a")
if node.children[i] is None:
node.children[i] = TrieNode()
node = node.children[i]
node.is_word = True
def shortest_root(trie: TrieNode, word: str) -> str:
"""The shortest stored root that `word` starts with, or `word` itself if none does."""
node = trie
for i in range(len(word)):
child = node.children[ord(word[i]) - ord("a")]
if child is None:
return word # No root continues this way.
# Prefixes come up shortest first, so the first root found is the answer.
if child.is_word:
return word[: i + 1]
node = child
return word # The whole word is a path, but no root ends on it.
def replace_words(roots: list[str], sentence: str) -> str:
"""Replaces each word in `sentence` with the shortest root it starts with.
Words that start with no root stay as they are."""
trie = TrieNode()
for root in roots:
insert(trie, root)
words = sentence.split(" ")
for i in range(len(words)):
words[i] = shortest_root(trie, words[i])
return " ".join(words)
if __name__ == "__main__":
print(replace_words(["cat", "bat", "rat"], "the cattle was rattled by the battery")) # the cat was rat by the bat
print(replace_words(["a", "b", "c"], "aadsfasf absbs bbab cadsfafs")) # a a b c
print(replace_words(["car", "ca", "dog"], "cart cab dot")) # ca ca dotclass TrieNode {
// One slot per lowercase letter. null means no stored word continues that way.
children: (TrieNode | null)[] = new Array<TrieNode | null>(26).fill(null);
isWord = false; // Whether a stored root ends exactly here.
}
// Adds `word` to the trie rooted at `trie`.
function insert(trie: TrieNode, word: string): void {
let node = trie;
for (const ch of word) {
const i = ch.charCodeAt(0) - "a".charCodeAt(0);
node = node.children[i] ??= new TrieNode();
}
node.isWord = true;
}
// The shortest stored root that `word` starts with, or `word` itself if none does.
function shortestRoot(trie: TrieNode, word: string): string {
let node = trie;
for (let i = 0; i < word.length; i++) {
const child = node.children[word.charCodeAt(i) - "a".charCodeAt(0)];
if (child === null) return word; // No root continues this way.
// Prefixes come up shortest first, so the first root found is the answer.
if (child.isWord) return word.slice(0, i + 1);
node = child;
}
return word; // The whole word is a path, but no root ends on it.
}
// Replaces each word in `sentence` with the shortest root it starts with.
// Words that start with no root stay as they are.
export function replaceWords(roots: string[], sentence: string): string {
const trie = new TrieNode();
for (const root of roots) insert(trie, root);
const words = sentence.split(" ");
for (let i = 0; i < words.length; i++) words[i] = shortestRoot(trie, words[i]);
return words.join(" ");
}
console.log(replaceWords(["cat", "bat", "rat"], "the cattle was rattled by the battery")); // the cat was rat by the bat
console.log(replaceWords(["a", "b", "c"], "aadsfasf absbs bbab cadsfafs")); // a a b c
console.log(replaceWords(["car", "ca", "dog"], "cart cab dot")); // ca ca dotpackage main
import (
"fmt"
"strings"
)
// TrieNode is one node of a trie.
type TrieNode struct {
// One slot per lowercase letter. nil means no stored word continues that way.
children [26]*TrieNode
isWord bool // Whether a stored root ends exactly here.
}
// insert adds word to the trie rooted at trie.
func insert(trie *TrieNode, word string) {
node := trie
for _, ch := range word {
i := ch - 'a'
if node.children[i] == nil {
node.children[i] = &TrieNode{}
}
node = node.children[i]
}
node.isWord = true
}
// shortestRoot returns the shortest stored root that word starts with, or
// word itself if none does.
func shortestRoot(trie *TrieNode, word string) string {
node := trie
for i := 0; i < len(word); i++ {
child := node.children[word[i]-'a']
if child == nil {
return word // No root continues this way.
}
// Prefixes come up shortest first, so the first root found is the answer.
if child.isWord {
return word[:i+1]
}
node = child
}
return word // The whole word is a path, but no root ends on it.
}
// replaceWords replaces each word in sentence with the shortest root it
// starts with. Words that start with no root stay as they are.
func replaceWords(roots []string, sentence string) string {
trie := &TrieNode{}
for _, root := range roots {
insert(trie, root)
}
words := strings.Split(sentence, " ")
for i := range words {
words[i] = shortestRoot(trie, words[i])
}
return strings.Join(words, " ")
}
func main() {
fmt.Println(replaceWords([]string{"cat", "bat", "rat"}, "the cattle was rattled by the battery")) // the cat was rat by the bat
fmt.Println(replaceWords([]string{"a", "b", "c"}, "aadsfasf absbs bbab cadsfafs")) // a a b c
fmt.Println(replaceWords([]string{"car", "ca", "dog"}, "cart cab dot")) // ca ca dot
}using System;
public class Solution
{
public class TrieNode
{
// One slot per lowercase letter. null means no stored word continues that way.
public TrieNode?[] Children = new TrieNode?[26];
public bool IsWord; // Whether a stored root ends exactly here.
}
// Adds word to the trie rooted at trie.
static void Insert(TrieNode trie, string word)
{
var node = trie;
foreach (char ch in word)
{
int i = ch - 'a';
node = node.Children[i] ??= new TrieNode();
}
node.IsWord = true;
}
// The shortest stored root that word starts with, or word itself if none does.
static string ShortestRoot(TrieNode trie, string word)
{
var node = trie;
for (int i = 0; i < word.Length; i++)
{
var child = node.Children[word[i] - 'a'];
if (child == null) return word; // No root continues this way.
// Prefixes come up shortest first, so the first root found is the answer.
if (child.IsWord) return word.Substring(0, i + 1);
node = child;
}
return word; // The whole word is a path, but no root ends on it.
}
// Replaces each word in sentence with the shortest root it starts with.
// Words that start with no root stay as they are.
public static string ReplaceWords(string[] roots, string sentence)
{
var trie = new TrieNode();
foreach (var root in roots) Insert(trie, root);
var words = sentence.Split(' ');
for (int i = 0; i < words.Length; i++) words[i] = ShortestRoot(trie, words[i]);
return string.Join(" ", words);
}
public static void Main()
{
Console.WriteLine(ReplaceWords(new[] { "cat", "bat", "rat" }, "the cattle was rattled by the battery")); // the cat was rat by the bat
Console.WriteLine(ReplaceWords(new[] { "a", "b", "c" }, "aadsfasf absbs bbab cadsfafs")); // a a b c
Console.WriteLine(ReplaceWords(new[] { "car", "ca", "dog" }, "cart cab dot")); // ca ca dot
}
}public class Solution {
static class TrieNode {
// One slot per lowercase letter. null means no stored word continues that way.
TrieNode[] children = new TrieNode[26];
boolean isWord; // Whether a stored root ends exactly here.
}
// Adds word to the trie rooted at trie.
static void insert(TrieNode trie, String word) {
TrieNode node = trie;
for (char ch : word.toCharArray()) {
int i = ch - 'a';
if (node.children[i] == null) node.children[i] = new TrieNode();
node = node.children[i];
}
node.isWord = true;
}
// The shortest stored root that word starts with, or word itself if none does.
static String shortestRoot(TrieNode trie, String word) {
TrieNode node = trie;
for (int i = 0; i < word.length(); i++) {
TrieNode child = node.children[word.charAt(i) - 'a'];
if (child == null) return word; // No root continues this way.
// Prefixes come up shortest first, so the first root found is the answer.
if (child.isWord) return word.substring(0, i + 1);
node = child;
}
return word; // The whole word is a path, but no root ends on it.
}
// Replaces each word in sentence with the shortest root it starts with.
// Words that start with no root stay as they are.
static String replaceWords(String[] roots, String sentence) {
TrieNode trie = new TrieNode();
for (String root : roots) insert(trie, root);
String[] words = sentence.split(" ");
for (int i = 0; i < words.length; i++) words[i] = shortestRoot(trie, words[i]);
return String.join(" ", words);
}
public static void main(String[] args) {
System.out.println(replaceWords(new String[] {"cat", "bat", "rat"}, "the cattle was rattled by the battery")); // the cat was rat by the bat
System.out.println(replaceWords(new String[] {"a", "b", "c"}, "aadsfasf absbs bbab cadsfafs")); // a a b c
System.out.println(replaceWords(new String[] {"car", "ca", "dog"}, "cart cab dot")); // ca ca dot
}
}Common Pitfalls
- Treating a path as a word. After inserting "apple", the path for "app" exists, so
searchmust check the end-of-word flag. - Forgetting the flag when a new word is a prefix of an old one. Inserting "app" after "apple" creates no nodes, but it still has to mark the second
p. - Using a 26-slot array when the input can hold capitals, digits or other characters.
ch - 'a'then lands outside the array. Check the alphabet, or use a hash map. - Following only one child at a wildcard. A dot has to try every branch, and back up when one dead-ends.
- Building a trie when a hash set would do. If you only ever check whole words, a set is simpler and lighter. Reach for a trie when prefixes matter.
Practice
More problems to try on your own.