LeetCode 1268 - Search Suggestions System
This problem asks us to build a product suggestion system similar to what appears in e commerce search bars. We are give
Difficulty: 🟡 Medium
Topics: Array, String, Binary Search, Trie, Sorting, Heap (Priority Queue)
Solution
LeetCode 1268 - Search Suggestions System
Problem Understanding
This problem asks us to build a product suggestion system similar to what appears in e commerce search bars. We are given two inputs:
products, an array of unique product namessearchWord, the word the user types character by character
After each character is typed, we must return up to three product names that start with the current prefix. The suggestions must be:
- Prefix matches of the currently typed string
- Sorted lexicographically
- Limited to at most three results
The output is therefore a list where:
- The first element contains suggestions after typing the first character
- The second element contains suggestions after typing the first two characters
- And so on until the entire
searchWordis typed
For example, if the search word is "mouse":
- After typing
"m", we return the best matches starting with"m" - After typing
"mo", we return the best matches starting with"mo" - After typing
"mou", we narrow the matches further
The constraints are important:
- Up to 1000 products
- Total characters across all products are at most
2 * 10^4 - Search word length can be up to 1000
These limits are not extremely large, but they are large enough that repeatedly scanning every product for every prefix would become inefficient.
The problem guarantees that:
- All product names are unique
- All strings contain only lowercase English letters
This simplifies the implementation because we do not need to handle duplicates or special characters.
Several edge cases are important:
- Only one product exists
- No products match after some prefix
- Many products share the same prefix
- The search word becomes more specific and reduces matches
- Product names may be much longer than the search word
A naive implementation can easily waste time repeatedly scanning the full product list for every prefix.
Approaches
Brute Force Approach
The simplest solution is to generate every prefix of searchWord and scan the entire products array each time.
For every prefix:
- Check every product
- Keep products whose names start with the prefix
- Sort the matches lexicographically
- Take the first three products
This approach is correct because it explicitly evaluates every possible candidate for every prefix.
However, it is inefficient because the same products are repeatedly scanned and sorted for every prefix. If the search word has length m and there are n products, we repeatedly perform expensive prefix checks and sorting operations.
Optimal Approach
The key observation is that lexicographical ordering is extremely useful here.
If we sort the products once at the beginning:
- Products sharing the same prefix become grouped together
- The smallest lexicographical products naturally appear first
Then, for each prefix, we can use binary search to locate the first product that could match that prefix.
Because matching products appear consecutively in sorted order, we only need to inspect the next few products after the binary search result. Since we only need at most three suggestions, we check at most three candidates.
This dramatically reduces repeated work.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(m * n * log n) | O(n) | Repeatedly scans and sorts matching products |
| Optimal | O(n log n + m log n) | O(1) excluding output | Sort once, then use binary search for each prefix |
Where:
nis the number of productsmis the length ofsearchWord
Algorithm Walkthrough
Optimal Algorithm
- Sort the
productsarray lexicographically.
This is the most important preprocessing step. After sorting, all products with the same prefix become contiguous in the array, and the lexicographically smallest products appear first automatically. 2. Initialize an empty result list.
Each entry in this result list will correspond to suggestions after typing one additional character. 3. Build prefixes incrementally.
Start with an empty string. For every character in searchWord, append it to the current prefix.
For example:
"m""mo""mou""mous""mouse"
- Use binary search to find the insertion position of the current prefix.
Since the array is sorted, binary search can quickly locate the leftmost position where the prefix could appear.
In Python, this is done with bisect_left.
In Go, we implement the binary search manually using sort.SearchStrings.
5. Collect up to three matching products.
Starting from the binary search index:
- Check the current product
- Verify whether it starts with the prefix
- Add it to the suggestions if it matches
- Stop after collecting three suggestions
Since matching products are contiguous in sorted order, the first three valid matches are guaranteed to be the correct answer. 6. Append the suggestions list to the final result. 7. Continue until all prefixes are processed.
Why it works
The algorithm works because lexicographical sorting guarantees that all strings sharing the same prefix appear consecutively. Binary search finds the first possible matching position efficiently, and scanning forward retrieves the smallest matching products in sorted order. Since we only need three suggestions, checking a few nearby elements is sufficient.
Python Solution
from bisect import bisect_left
from typing import List
class Solution:
def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]:
products.sort()
result = []
prefix = ""
for char in searchWord:
prefix += char
start_index = bisect_left(products, prefix)
suggestions = []
for i in range(start_index, min(start_index + 3, len(products))):
if products[i].startswith(prefix):
suggestions.append(products[i])
result.append(suggestions)
return result
The implementation begins by sorting the products array. This ensures lexicographical ordering and groups matching prefixes together.
The variable prefix is built incrementally as characters are processed from searchWord. For every new prefix, bisect_left performs binary search to locate the first position where the prefix could appear.
From that index, we inspect at most three products. Since the array is sorted, the first three valid matches are automatically the correct suggestions.
The startswith check is still necessary because binary search only guarantees the insertion position, not that the product actually matches the prefix.
Finally, each suggestion list is appended to the result.
Go Solution
package main
import (
"sort"
"strings"
)
func suggestedProducts(products []string, searchWord string) [][]string {
sort.Strings(products)
result := [][]string{}
prefix := ""
for _, ch := range searchWord {
prefix += string(ch)
startIndex := sort.SearchStrings(products, prefix)
suggestions := []string{}
for i := startIndex; i < len(products) && i < startIndex+3; i++ {
if strings.HasPrefix(products[i], prefix) {
suggestions = append(suggestions, products[i])
}
}
result = append(result, suggestions)
}
return result
}
The Go solution follows the same strategy as the Python implementation.
The main difference is that Go does not have a built in bisect_left equivalent, so we use sort.SearchStrings, which performs binary search on a sorted slice.
Prefix checking is done using strings.HasPrefix.
Slices in Go naturally handle dynamic growth when appending suggestions.
Worked Examples
Example 1
Input:
products = ["mobile","mouse","moneypot","monitor","mousepad"]
searchWord = "mouse"
After sorting:
["mobile","moneypot","monitor","mouse","mousepad"]
| Prefix | Binary Search Position | Matching Products | Suggestions |
|---|---|---|---|
| "m" | 0 | all products | ["mobile","moneypot","monitor"] |
| "mo" | 0 | all products | ["mobile","moneypot","monitor"] |
| "mou" | 3 | ["mouse","mousepad"] | ["mouse","mousepad"] |
| "mous" | 3 | ["mouse","mousepad"] | ["mouse","mousepad"] |
| "mouse" | 3 | ["mouse","mousepad"] | ["mouse","mousepad"] |
Final output:
[
["mobile","moneypot","monitor"],
["mobile","moneypot","monitor"],
["mouse","mousepad"],
["mouse","mousepad"],
["mouse","mousepad"]
]
Example 2
Input:
products = ["havana"]
searchWord = "havana"
Sorted products:
["havana"]
| Prefix | Binary Search Position | Suggestions |
|---|---|---|
| "h" | 0 | ["havana"] |
| "ha" | 0 | ["havana"] |
| "hav" | 0 | ["havana"] |
| "hava" | 0 | ["havana"] |
| "havan" | 0 | ["havana"] |
| "havana" | 0 | ["havana"] |
Final output:
[
["havana"],
["havana"],
["havana"],
["havana"],
["havana"],
["havana"]
]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n + m log n) | Sorting dominates preprocessing, each prefix uses binary search |
| Space | O(1) excluding output | Only a few temporary variables are used |
The sorting step costs O(n log n).
For each prefix of length m, binary search takes O(log n). We then inspect at most three products, which is constant time.
Therefore, the total complexity becomes:
O(n log n + m log n)
The algorithm uses only constant extra space aside from the output list.
Test Cases
from typing import List
class Solution:
def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]:
from bisect import bisect_left
products.sort()
result = []
prefix = ""
for char in searchWord:
prefix += char
start_index = bisect_left(products, prefix)
suggestions = []
for i in range(start_index, min(start_index + 3, len(products))):
if products[i].startswith(prefix):
suggestions.append(products[i])
result.append(suggestions)
return result
solution = Solution()
# Example 1
assert solution.suggestedProducts(
["mobile","mouse","moneypot","monitor","mousepad"],
"mouse"
) == [
["mobile","moneypot","monitor"],
["mobile","moneypot","monitor"],
["mouse","mousepad"],
["mouse","mousepad"],
["mouse","mousepad"]
] # standard example with multiple matches
# Example 2
assert solution.suggestedProducts(
["havana"],
"havana"
) == [
["havana"],
["havana"],
["havana"],
["havana"],
["havana"],
["havana"]
] # single product case
# No matching products after first character
assert solution.suggestedProducts(
["apple", "banana", "carrot"],
"dog"
) == [
[],
[],
[]
] # no matches at all
# Products sharing long prefixes
assert solution.suggestedProducts(
["app", "apple", "application", "apply"],
"app"
) == [
["app", "apple", "application"],
["app", "apple", "application"],
["app", "apple", "application"]
] # verifies lexicographical ordering
# Fewer than three matches
assert solution.suggestedProducts(
["bags", "baggage", "banner"],
"bags"
) == [
["baggage", "bags", "banner"],
["baggage", "bags"],
["bags"],
["bags"]
] # shrinking result set
# Exact match only
assert solution.suggestedProducts(
["cat", "dog", "fish"],
"cat"
) == [
["cat"],
["cat"],
["cat"]
] # exact single match
# Prefix becomes invalid midway
assert solution.suggestedProducts(
["hello", "help", "helmet"],
"hex"
) == [
["hello", "helmet", "help"],
["hello", "helmet", "help"],
[]
] # valid early prefixes but invalid later
Test Summary
| Test | Why |
|---|---|
| Standard mouse example | Verifies core functionality |
| Single product | Ensures repeated matching works |
| No matches | Verifies empty suggestion handling |
| Shared prefixes | Tests lexicographical ordering |
| Fewer than three matches | Ensures partial suggestion lists work |
| Exact match | Confirms exact prefix behavior |
| Prefix becomes invalid | Tests transition from valid to empty results |
Edge Cases
No Matching Products
A common bug occurs when no products match a prefix. Some implementations may continue scanning unnecessarily or accidentally include unrelated products.
For example:
products = ["apple", "banana"]
searchWord = "cat"
Binary search locates the insertion position, but the startswith check prevents unrelated products from being added. The implementation correctly returns empty lists for all prefixes.
Fewer Than Three Suggestions
The problem requires returning at most three suggestions, not exactly three.
For example:
products = ["mouse", "mousepad"]
searchWord = "mouse"
Only two matching products exist. The algorithm naturally stops when it reaches the array boundary or runs out of matching products.
Prefix Stops Matching Midway
Another tricky case occurs when early prefixes match but later prefixes do not.
For example:
products = ["hello", "help", "helmet"]
searchWord = "hex"
The prefixes "h" and "he" match several products, but "hex" matches none.
The implementation handles this correctly because every prefix performs a fresh binary search and validates matches independently.
Lexicographical Ordering
A naive implementation might collect matching products in original input order instead of lexicographical order.
Sorting the array once at the beginning guarantees that the first matching products encountered are always the lexicographically smallest valid suggestions.