720. Longest Word in Dictionary
Problem Description
You are given an array of strings words
representing an English Dictionary. Your task is to find the longest word in this dictionary that can be built one character at a time by other words in the dictionary.
The key requirement is that the word must be buildable incrementally from left to right. This means:
- To build a word of length
n
, all its prefixes of lengths1, 2, 3, ..., n-1
must also exist in the dictionary - Each step adds exactly one character to the end of an existing word
For example, if words = ["a", "ap", "app", "apple"]
, then "apple" can be built because:
- "a" exists in the dictionary
- "ap" exists (built by adding 'p' to "a")
- "app" exists (built by adding 'p' to "ap")
- "apple" exists (built by adding 'l' and 'e' to "app")
If multiple words have the same maximum length, return the one that comes first lexicographically (alphabetically). If no word can be built this way, return an empty string.
The solution uses a Trie data structure to efficiently store and search for words. The Trie's search
method is modified to check that every prefix of a word (not just the complete word) exists as a complete word in the dictionary by verifying the is_end
flag at each character. This ensures the word can be built incrementally as required.
Intuition
The core insight is recognizing that we need to verify if every prefix of a word exists as a complete word in the dictionary. When we think about checking prefixes efficiently, a Trie naturally comes to mind because it stores words in a prefix-tree structure where common prefixes are shared.
Consider how we would check if a word like "apple" can be built incrementally:
- We need to verify "a" is a complete word
- Then verify "ap" is a complete word
- Then verify "app" is a complete word
- Then verify "appl" is a complete word
- Finally verify "apple" is a complete word
A naive approach would require searching the entire word list for each prefix, resulting in inefficient repeated searches. However, with a Trie, we can traverse character by character and check at each node whether it marks the end of a valid word using the is_end
flag.
The key modification to the standard Trie search is that instead of just checking if the final node has is_end = True
, we need to verify that EVERY intermediate node along the path also has is_end = True
. This ensures all prefixes are valid words.
Once we have this efficient way to verify if a word can be built incrementally, we simply iterate through all words, check each one, and keep track of the longest valid word. When we encounter words of equal length, we update our answer only if the new word is lexicographically smaller (using the string comparison ans > w
).
The Trie structure makes this solution efficient because we only build the Trie once with O(total characters)
time, and then each word verification takes O(word length)
time by walking down the Trie path once.
Solution Approach
The solution uses a Trie (prefix tree) data structure to efficiently store and search for words. Let's walk through the implementation step by step:
Trie Implementation
First, we define a [Trie](/problems/trie_intro)
class with the following components:
-
Node Structure: Each Trie node contains:
children
: An array of 26 elements (for letters 'a' to 'z'), wherechildren[i]
points to the child node for characterchr(ord('a') + i)
is_end
: A boolean flag indicating if this node marks the end of a valid word
-
Insert Method:
def insert(self, w: str): node = self for c in w: idx = ord(c) - ord("a") # Convert character to index (0-25) if node.children[idx] is None: node.children[idx] = [Trie](/problems/trie_intro)() # Create new node if needed node = node.children[idx] node.is_end = True # Mark the end of the word
-
Modified Search Method:
def search(self, w: str) -> bool: node = self for c in w: idx = ord(c) - ord("a") if node.children[idx] is None: return False # Character path doesn't exist node = node.children[idx] if not node.is_end: return False # Prefix is not a complete word return True
The crucial modification here is checking
node.is_end
at each step, ensuring every prefix is a complete word in the dictionary.
Main Solution Logic
-
Build the Trie: Insert all words from the input array into the Trie
trie = Trie() for w in words: trie.insert(w)
-
Find the Longest Valid Word: Iterate through all words and check if each can be built incrementally
ans = "" for w in words: if [trie](/problems/trie_intro).search(w): # Check if word can be built incrementally if len(ans) < len(w) or (len(ans) == len(w) and ans > w): ans = w
The update condition ensures we keep:
- The longest word when lengths differ (
len(ans) < len(w)
) - The lexicographically smallest word when lengths are equal (
len(ans) == len(w) and ans > w
)
- The longest word when lengths differ (
Time and Space Complexity
-
Time Complexity:
O(n ร m)
wheren
is the number of words andm
is the average length of words- Building the Trie:
O(n ร m)
- Searching all words:
O(n ร m)
- Building the Trie:
-
Space Complexity:
O(ALPHABET_SIZE ร n ร m)
for storing the Trie structure, whereALPHABET_SIZE = 26
This approach efficiently solves the problem by leveraging the Trie's ability to share common prefixes and quickly verify if all prefixes of a word exist as complete words in the dictionary.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with words = ["w", "wo", "wor", "worl", "world", "a", "ap", "app"]
.
Step 1: Build the Trie
We insert each word into the Trie. After insertion, the Trie structure looks like this (showing only relevant paths):
root
/ \
w a
/ \
o(end) p(end)
/ \
r(end) p(end)
/
l(end)
\
d(end)
Each node marked with (end)
has is_end = True
, indicating a complete word ends there.
Step 2: Check Each Word
Now we iterate through each word and use our modified search
method:
-
"w":
- Check 'w': Node exists but
is_end = False
โ ReturnFalse
- Cannot be built incrementally (no single letter "w" in dictionary)
- Check 'w': Node exists but
-
"wo":
- Check 'w': Node exists but
is_end = False
โ ReturnFalse
- Cannot be built (needs "w" to exist first)
- Check 'w': Node exists but
-
"wor":
- Check 'w': Node exists but
is_end = False
โ ReturnFalse
- Cannot be built
- Check 'w': Node exists but
-
"worl":
- Check 'w': Node exists but
is_end = False
โ ReturnFalse
- Cannot be built
- Check 'w': Node exists but
-
"world":
- Check 'w': Node exists but
is_end = False
โ ReturnFalse
- Cannot be built
- Check 'w': Node exists but
-
"a":
- Check 'a': Node exists and
is_end = True
โ Continue - Reached end of word โ Return
True
- Valid! Update
ans = "a"
- Check 'a': Node exists and
-
"ap":
- Check 'a': Node exists and
is_end = True
โ Continue - Check 'p': Node exists and
is_end = True
โ Continue - Reached end of word โ Return
True
- Valid! Since
len("ap") > len("a")
, updateans = "ap"
- Check 'a': Node exists and
-
"app":
- Check 'a': Node exists and
is_end = True
โ Continue - Check 'p': Node exists and
is_end = True
โ Continue - Check 'p': Node exists and
is_end = True
โ Continue - Reached end of word โ Return
True
- Valid! Since
len("app") > len("ap")
, updateans = "app"
- Check 'a': Node exists and
Final Result: "app"
is the longest word that can be built incrementally.
The key insight is that "world" cannot be built even though it's the longest word, because its prefixes "w", "wo", "wor", and "worl" don't all exist as complete words in the dictionary. In contrast, "app" can be built because:
- "a" exists as a complete word
- "ap" exists and can be built by adding 'p' to "a"
- "app" exists and can be built by adding 'p' to "ap"
Solution Implementation
1from typing import List, Optional
2
3
4class Trie:
5 """A trie (prefix tree) data structure for efficient string storage and retrieval."""
6
7 def __init__(self):
8 # Array to store 26 child nodes (one for each lowercase letter)
9 self.children: List[Optional[Trie]] = [None] * 26
10 # Flag to mark if current node represents end of a word
11 self.is_end: bool = False
12
13 def insert(self, word: str) -> None:
14 """
15 Insert a word into the trie.
16
17 Args:
18 word: The word to insert into the trie
19 """
20 current_node = self
21
22 # Traverse through each character in the word
23 for char in word:
24 # Calculate index for the character (0-25 for a-z)
25 index = ord(char) - ord('a')
26
27 # Create new node if path doesn't exist
28 if current_node.children[index] is None:
29 current_node.children[index] = Trie()
30
31 # Move to the child node
32 current_node = current_node.children[index]
33
34 # Mark the last node as end of word
35 current_node.is_end = True
36
37 def search(self, word: str) -> bool:
38 """
39 Check if a word can be built character by character where each prefix is also a word.
40
41 Args:
42 word: The word to search for
43
44 Returns:
45 True if word exists and all its prefixes are valid words, False otherwise
46 """
47 current_node = self
48
49 # Traverse through each character in the word
50 for char in word:
51 # Calculate index for the character
52 index = ord(char) - ord('a')
53
54 # If path doesn't exist, word cannot be formed
55 if current_node.children[index] is None:
56 return False
57
58 # Move to the child node
59 current_node = current_node.children[index]
60
61 # Check if current prefix is a valid word
62 # Every prefix must be a valid word for the word to be buildable
63 if not current_node.is_end:
64 return False
65
66 return True
67
68
69class Solution:
70 """Solution for finding the longest word that can be built one character at a time."""
71
72 def longestWord(self, words: List[str]) -> str:
73 """
74 Find the longest word that can be built one character at a time from other words.
75
76 Args:
77 words: List of words to process
78
79 Returns:
80 The longest word that can be built progressively;
81 if tie, returns the lexicographically smallest one
82 """
83 # Build trie with all words
84 trie = Trie()
85 for word in words:
86 trie.insert(word)
87
88 # Track the best answer found so far
89 longest_word = ""
90
91 # Check each word to see if it can be built progressively
92 for word in words:
93 # Check if word can be built and if it's better than current answer
94 if trie.search(word):
95 # Update answer if current word is longer,
96 # or same length but lexicographically smaller
97 if (len(longest_word) < len(word) or
98 (len(longest_word) == len(word) and longest_word > word)):
99 longest_word = word
100
101 return longest_word
102
1/**
2 * Trie (Prefix Tree) data structure implementation
3 * Used for efficient string prefix searching and validation
4 */
5class Trie {
6 // Array to store 26 children nodes (one for each lowercase letter a-z)
7 private Trie[] children = new Trie[26];
8 // Flag to mark if current node represents end of a word
9 private boolean isEnd = false;
10
11 /**
12 * Inserts a word into the trie
13 * @param word The word to insert
14 */
15 public void insert(String word) {
16 Trie currentNode = this;
17
18 // Traverse each character in the word
19 for (char character : word.toCharArray()) {
20 // Calculate array index (0-25) for the character
21 int index = character - 'a';
22
23 // Create new node if path doesn't exist
24 if (currentNode.children[index] == null) {
25 currentNode.children[index] = new Trie();
26 }
27
28 // Move to the child node
29 currentNode = currentNode.children[index];
30 }
31
32 // Mark the last node as end of word
33 currentNode.isEnd = true;
34 }
35
36 /**
37 * Searches for a word in the trie with special condition:
38 * Every prefix of the word must also be a complete word
39 * @param word The word to search
40 * @return true if word exists and all its prefixes are complete words
41 */
42 public boolean search(String word) {
43 Trie currentNode = this;
44
45 // Check each character in the word
46 for (char character : word.toCharArray()) {
47 // Calculate array index for the character
48 int index = character - 'a';
49
50 // Return false if path doesn't exist or prefix is not a complete word
51 if (currentNode.children[index] == null || !currentNode.children[index].isEnd) {
52 return false;
53 }
54
55 // Move to the child node
56 currentNode = currentNode.children[index];
57 }
58
59 return true;
60 }
61}
62
63/**
64 * Solution class for finding the longest word that can be built
65 * one character at a time by other words in the array
66 */
67class Solution {
68 /**
69 * Finds the longest word where every prefix is also a word in the array
70 * If multiple words have same length, returns the lexicographically smallest
71 * @param words Array of words to process
72 * @return The longest valid word, or empty string if none exists
73 */
74 public String longestWord(String[] words) {
75 // Initialize trie and insert all words
76 Trie trie = new Trie();
77 for (String word : words) {
78 trie.insert(word);
79 }
80
81 // Track the best answer found so far
82 String answer = "";
83
84 // Check each word to see if it's valid
85 for (String word : words) {
86 // Word is valid if all its prefixes exist as complete words
87 if (trie.search(word)) {
88 // Update answer if current word is longer
89 // or same length but lexicographically smaller
90 if (answer.length() < word.length() ||
91 (answer.length() == word.length() && word.compareTo(answer) < 0)) {
92 answer = word;
93 }
94 }
95 }
96
97 return answer;
98 }
99}
100
1class Trie {
2public:
3 // Array to store pointers to child nodes (26 letters)
4 Trie* children[26] = {nullptr};
5 // Flag to mark if current node represents end of a word
6 bool isEnd = false;
7
8 /**
9 * Inserts a word into the trie
10 * @param word - The word to be inserted
11 */
12 void insert(const string& word) {
13 Trie* currentNode = this;
14
15 // Traverse through each character in the word
16 for (char ch : word) {
17 int index = ch - 'a'; // Convert character to array index (0-25)
18
19 // Create new node if path doesn't exist
20 if (currentNode->children[index] == nullptr) {
21 currentNode->children[index] = new Trie();
22 }
23
24 // Move to the child node
25 currentNode = currentNode->children[index];
26 }
27
28 // Mark the last node as end of word
29 currentNode->isEnd = true;
30 }
31
32 /**
33 * Searches if a word can be built character by character
34 * Each prefix of the word must also be a valid word (isEnd = true)
35 * @param word - The word to search
36 * @return true if word can be built progressively, false otherwise
37 */
38 bool search(const string& word) {
39 Trie* currentNode = this;
40
41 // Check each character and its prefix
42 for (char ch : word) {
43 int index = ch - 'a'; // Convert character to array index
44
45 // If path doesn't exist or prefix is not a valid word, return false
46 if (currentNode->children[index] == nullptr ||
47 !currentNode->children[index]->isEnd) {
48 return false;
49 }
50
51 // Move to the child node
52 currentNode = currentNode->children[index];
53 }
54
55 return true;
56 }
57};
58
59class Solution {
60public:
61 /**
62 * Finds the longest word that can be built one character at a time
63 * from other words in the array
64 * @param words - Vector of words to process
65 * @return The longest valid word (lexicographically smallest if tie)
66 */
67 string longestWord(vector<string>& words) {
68 // Build trie with all words
69 Trie trie;
70 for (const string& word : words) {
71 trie.insert(word);
72 }
73
74 // Find the longest word that can be built progressively
75 string result = "";
76 for (const string& word : words) {
77 // Check if word can be built and update result if it's better
78 if (trie.search(word)) {
79 // Update if current word is longer, or same length but lexicographically smaller
80 if (result.length() < word.length() ||
81 (result.length() == word.length() && word < result)) {
82 result = word;
83 }
84 }
85 }
86
87 return result;
88 }
89};
90
1// Trie node structure with children array and end-of-word flag
2interface TrieNode {
3 children: (TrieNode | null)[];
4 isEnd: boolean;
5}
6
7// Create a new trie node with 26 null children (for lowercase letters a-z)
8function createTrieNode(): TrieNode {
9 return {
10 children: new Array(26).fill(null),
11 isEnd: false
12 };
13}
14
15// Insert a word into the trie
16function insert(root: TrieNode, word: string): void {
17 let currentNode: TrieNode = root;
18
19 // Traverse through each character in the word
20 for (let i = 0; i < word.length; i++) {
21 // Calculate the index for the current character (0-25 for a-z)
22 const charIndex: number = word.charCodeAt(i) - 'a'.charCodeAt(0);
23
24 // Create a new node if it doesn't exist
25 if (currentNode.children[charIndex] === null) {
26 currentNode.children[charIndex] = createTrieNode();
27 }
28
29 // Move to the next node
30 currentNode = currentNode.children[charIndex]!;
31 }
32
33 // Mark the end of the word
34 currentNode.isEnd = true;
35}
36
37// Search for a word in the trie, ensuring all prefixes are also complete words
38function search(root: TrieNode, word: string): boolean {
39 let currentNode: TrieNode = root;
40
41 // Traverse through each character in the word
42 for (let i = 0; i < word.length; i++) {
43 // Calculate the index for the current character
44 const charIndex: number = word.charCodeAt(i) - 'a'.charCodeAt(0);
45
46 // Check if the path exists and each prefix is a complete word
47 if (currentNode.children[charIndex] === null ||
48 !currentNode.children[charIndex]!.isEnd) {
49 return false;
50 }
51
52 // Move to the next node
53 currentNode = currentNode.children[charIndex]!;
54 }
55
56 return true;
57}
58
59// Find the longest word where every prefix is also a word in the array
60function longestWord(words: string[]): string {
61 // Initialize the trie root
62 const trieRoot: TrieNode = createTrieNode();
63
64 // Insert all words into the trie
65 for (const word of words) {
66 insert(trieRoot, word);
67 }
68
69 // Track the longest valid word
70 let longestValidWord: string = '';
71
72 // Check each word to see if all its prefixes exist as complete words
73 for (const word of words) {
74 // If the word is valid and is either longer than current best
75 // or same length but lexicographically smaller
76 if (search(trieRoot, word) &&
77 (longestValidWord.length < word.length ||
78 (longestValidWord.length === word.length && word < longestValidWord))) {
79 longestValidWord = word;
80 }
81 }
82
83 return longestValidWord;
84}
85
Time and Space Complexity
Time Complexity: O(L)
The algorithm consists of two main phases:
-
Building the Trie: We iterate through all words and insert each one into the Trie. For each word, we traverse through its characters. Since we process every character of every word exactly once during insertion, this takes
O(L)
time, whereL
is the sum of lengths of all words. -
Searching in the Trie: We iterate through all words again and search each one. The
search
method traverses each character of the word and checks if all prefixes exist as complete words (using theis_end
flag). In the worst case, we examine every character of every word once, which is alsoO(L)
.
Therefore, the total time complexity is O(L) + O(L) = O(L)
.
Space Complexity: O(L)
The space is dominated by the Trie structure. In the worst case, when there are no common prefixes among words, each character of each word creates a new node in the Trie. Each node contains:
- An array of 26 pointers (for lowercase letters a-z)
- A boolean flag
is_end
However, the number of nodes created is bounded by the total number of characters across all words. Even though each node has an array of size 26, the total number of nodes cannot exceed L
(one node per character in the worst case). Therefore, the space complexity is O(L)
.
The additional space used for the ans
variable and temporary variables during traversal is O(max word length)
, which is bounded by O(L)
, so it doesn't affect the overall space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Prefix Validation Logic
The Pitfall: A common mistake is implementing the search method to only check if the final word exists in the trie, without verifying that every prefix is also a complete word. This would look like:
def search(self, word: str) -> bool:
current_node = self
for char in word:
index = ord(char) - ord('a')
if current_node.children[index] is None:
return False
current_node = current_node.children[index]
return current_node.is_end # Only checks if the final word exists
This implementation would incorrectly accept words like "apple" even if "ap" or "app" don't exist as complete words in the dictionary.
The Solution:
Check is_end
flag at every character position (except the starting empty prefix):
def search(self, word: str) -> bool:
current_node = self
for i, char in enumerate(word):
index = ord(char) - ord('a')
if current_node.children[index] is None:
return False
current_node = current_node.children[index]
# For non-empty prefixes, verify they're complete words
if not current_node.is_end:
return False
return True
2. Handling Single-Character Words Incorrectly
The Pitfall: Some implementations might skip checking single-character words or handle them as a special case, assuming they can always be built. However, a single-character word like "a" must actually exist in the dictionary to be valid.
The Solution:
The search method should work uniformly for all word lengths. Single-character words will naturally be validated by checking if they exist in the trie with is_end = True
.
3. Wrong Lexicographical Comparison
The Pitfall: When comparing words of the same length, using the wrong comparison operator:
# Incorrect - keeps lexicographically larger word
if len(longest_word) == len(word) and longest_word < word:
longest_word = word
The Solution: Use the correct comparison to keep the lexicographically smaller word:
# Correct - keeps lexicographically smaller word
if len(longest_word) == len(word) and longest_word > word:
longest_word = word
4. Not Handling Empty Input
The Pitfall: The code might fail when given an empty array or when no valid buildable word exists.
The Solution: Initialize the answer as an empty string, which naturally handles both cases:
- Empty input array โ returns empty string
- No buildable words โ returns empty string
5. Assuming Words Are Already Sorted
The Pitfall: Some might try to optimize by assuming the input is sorted by length or lexicographically, leading to early termination or incorrect results.
The Solution: Always check all words in the input array without making assumptions about their order. The current implementation correctly iterates through all words regardless of their order.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!