Facebook Pixel

720. Longest Word in Dictionary

MediumTrieArrayHash TableStringSorting
Leetcode Link

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 lengths 1, 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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.

Learn more about Trie and Sorting patterns.

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:

  1. Node Structure: Each Trie node contains:

    • children: An array of 26 elements (for letters 'a' to 'z'), where children[i] points to the child node for character chr(ord('a') + i)
    • is_end: A boolean flag indicating if this node marks the end of a valid word
  2. 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
  3. 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

  1. Build the Trie: Insert all words from the input array into the Trie

    trie = Trie()
    for w in words:
        trie.insert(w)
  2. 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)

Time and Space Complexity

  • Time Complexity: O(n ร— m) where n is the number of words and m is the average length of words

    • Building the Trie: O(n ร— m)
    • Searching all words: O(n ร— m)
  • Space Complexity: O(ALPHABET_SIZE ร— n ร— m) for storing the Trie structure, where ALPHABET_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 Evaluator

Example 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:

  1. "w":

    • Check 'w': Node exists but is_end = False โ†’ Return False
    • Cannot be built incrementally (no single letter "w" in dictionary)
  2. "wo":

    • Check 'w': Node exists but is_end = False โ†’ Return False
    • Cannot be built (needs "w" to exist first)
  3. "wor":

    • Check 'w': Node exists but is_end = False โ†’ Return False
    • Cannot be built
  4. "worl":

    • Check 'w': Node exists but is_end = False โ†’ Return False
    • Cannot be built
  5. "world":

    • Check 'w': Node exists but is_end = False โ†’ Return False
    • Cannot be built
  6. "a":

    • Check 'a': Node exists and is_end = True โ†’ Continue
    • Reached end of word โ†’ Return True
    • Valid! Update ans = "a"
  7. "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"), update ans = "ap"
  8. "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"), update ans = "app"

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:

  1. 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, where L is the sum of lengths of all words.

  2. 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 the is_end flag). In the worst case, we examine every character of every word once, which is also O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More