THN Interview Prep

648. Replace Words

At a Glance

  • Topic: tries
  • Pattern: Trie
  • Difficulty: Medium
  • Companies: Google, Amazon, Microsoft, Meta, Apple
  • Frequency: Medium
  • LeetCode: 648

Problem (one-liner)

You are given a list of roots (short strings) and a sentence of space-separated words. For each word, replace it with the shortest root in the dictionary that is a prefix of that word; if no root is a prefix, keep the word unchanged.

Recognition Cues

  • Prefix replacement from a static dictionary
  • Shortest matching prefix → walk the trie along the word and stop at the first terminal node on that path
  • Building the trie once amortizes many lookups

Diagram

At-a-glance flow (replace with problem-specific Mermaid as you refine this note). camelCase node IDs; no spaces in IDs.

Loading diagram…

Approaches

  • Scan all roots per wordO(words * roots * L) — too slow at scale.
  • Sort roots by length then try shortest firstO(R log R + words * L * something) — acceptable but trie is cleaner.
  • Trie — insert all roots, then each word walks until missing edge or hitting a root marker.

Optimal Solution

Go

package main

import "strings"

type Trie struct {
	children [26]*Trie
	isRoot   bool
}

func (trie *Trie) insert(root string) {
	node := trie
	for index := range root {
		offset := int(root[index] - 'a')
		if node.children[offset] == nil {
			node.children[offset] = &Trie{}
		}
		node = node.children[offset]
	}
	node.isRoot = true
}

func (trie *Trie) replaceWord(word string) string {
	node := trie
	for index := range word {
		offset := int(word[index] - 'a')
		if node.children[offset] == nil {
			return word
		}
		node = node.children[offset]
		if node.isRoot {
			return word[:index+1]
		}
	}
	return word
}

func replaceWords(roots []string, sentence string) string {
	trie := &Trie{}
	for _, root := range roots {
		trie.insert(root)
	}
	words := strings.Fields(sentence)
	for wordIndex := range words {
		words[wordIndex] = trie.replaceWord(words[wordIndex])
	}
	return strings.Join(words, " ")
}

JavaScript

class Trie {
	constructor() {
		this.children = new Map();
		this.isRoot = false;
	}

	insert(rootWord) {
		let node = this;
		for (let index = 0; index < rootWord.length; index++) {
			const letter = rootWord[index];
			if (!node.children.has(letter)) {
				node.children.set(letter, new Trie());
			}
			node = node.children.get(letter);
		}
		node.isRoot = true;
	}

	replaceWord(word) {
		let node = this;
		for (let index = 0; index < word.length; index++) {
			const letter = word[index];
			if (!node.children.has(letter)) {
				return word;
			}
			node = node.children.get(letter);
			if (node.isRoot) {
				return word.slice(0, index + 1);
			}
		}
		return word;
	}
}

function replaceWords(roots, sentence) {
	const trie = new Trie();
	for (const rootWord of roots) {
		trie.insert(rootWord);
	}
	return sentence
		.split(' ')
		.map((word) => trie.replaceWord(word))
		.join(' ');
}

Walkthrough

Roots ["cat","bat","rat"], sentence "the cattle was rattled by the battery".

tokenwalk stops whenoutput
theno shared prefix earlythe
cattleafter cat terminalcat
rattledafter ratrat
batteryafter batbat

Invariant: descending one character at a time, the first node marked isRoot along the token’s prefix yields the shortest dictionary root by construction of the path length.

Edge Cases

  • Root equals whole word → replacement identical
  • One root is prefix of another — shorter root terminates walk first (insert order does not matter if trie marks both terminals at correct depths)

Pitfalls

  • Returning the longest root instead of first hit — wrong problem interpretation
  • Splitting sentence incorrectly on multiple spaces—strings.Fields vs Split (match problem contract)

Similar Problems

Variants

  • Replace with longest root instead — walk full word tracking last terminal seen.
  • Case-insensitive or Unicode alphabets → change edge map.

Mind-Map Tags

#trie #prefix #replace #sentence #shortest-match

Last updated on

Spotted something unclear or wrong on this page?

On this page