We're building a simple autocomplete feature. As the user types each character of searchWord, we need to find all products that share the same prefix as what's been typed so far, then return up to 3 of them in lexicographic (alphabetical) order.
So if the user types "m", we want the 3 alphabetically smallest products starting with "m". Then they type "o" (prefix is now "mo"), and we want the 3 smallest products starting with "mo". And so on for each character.
We want the lexicographically smallest matches, not the most popular or most relevant. That requirement is what makes sorting useful. In a sorted array, all products with a common prefix sit next to each other in a contiguous block, already in alphabetical order. Finding that block efficiently is the core of the problem.
products.length <= 1000 and total character count <= 2 10^4. The input is small enough that even an O(n L) scan per character passes.searchWord.length <= 1000, so we answer up to 1000 prefix queries, one per character typed.For each prefix of searchWord, scan through all products, collect the ones that start with that prefix, sort them, and take the first 3. This directly mirrors the problem statement and needs no setup.
The wasteful part is the sorting. We re-sort the matching candidates every time a character is typed, even though the relative order of the products never changes between prefixes.
i from 0 to searchWord.length - 1, extract the prefix searchWord[0..i].Input:
For each prefix, we filter all products, sort matches, and take top 3:
The bottleneck is re-sorting matches on every keystroke. The next approach sorts the products once upfront, then uses binary search to locate the matching block directly.
Sort the products once, and every group of products sharing a prefix becomes a contiguous block. Searching for prefix "mo" reduces to finding where "mo" would be inserted. Everything from that insertion point onward that still starts with "mo" is a match, already in lexicographic order.
Sort once, then for each prefix use binary search to find the starting position. Check up to 3 products from that position. Because the array is sorted, the first 3 matches starting at the insertion point are the lexicographically smallest.
The lower bound for prefix "mo" is the first position where a string greater than or equal to "mo" appears. Any product starting with "mo" sorts at or after that position, and every product that starts with "mo" is contiguous from there. So if the product at the insertion point does not start with the prefix, no later product does either, which lets us stop after checking the first non-matching position. The first three matches we find are therefore the three smallest.
products array lexicographically.i from 0 to searchWord.length - 1, form the prefix searchWord[0..i].prefix could be inserted (lower bound).Binary search repeats a full O(log n) search for every character. Since the valid range only shrinks as the prefix grows, the next approach tracks that range with two pointers and narrows it incrementally instead of recomputing it.
The matching block on a sorted array can be tracked directly with two pointers instead of relocated by binary search each time. Maintain left and right bounding the range of products that match the current prefix. The full sorted array is in range initially. When character i is typed, advance left past products that are shorter than i + 1 or have the wrong character at position i, and retreat right past products that fail the same test from the other end.
Adding a character to the prefix can only remove products from the range, never add them, so the two pointers only move inward.
Because the array is sorted, the products matching a prefix form one contiguous block, and the block for a longer prefix is contained inside the block for any of its prefixes. Extending the prefix can only trim products off the two edges of the current block, never reintroduce one in the middle. So left never decreases and right never increases. Across all S characters, left advances at most n positions in total and right retreats at most n positions in total, which is O(n) pointer work on top of the sort, instead of the O(S * log n) of repeated binary searches.
products array lexicographically.left = 0 and right = products.length - 1.i from 0 to searchWord.length - 1:left <= right and (products[left].length <= i or products[left][i] != searchWord[i]), increment left.left <= right and (products[right].length <= i or products[right][i] != searchWord[i]), decrement right.left to min(left + 2, right).The two-pointer method is optimal for a single search word. When the product list is fixed and many different search words are queried against it, a Trie moves the per-character work down to a single pointer hop and precomputes the answer at every node.
Insert every product into a Trie keyed by character. At each node, store the three lexicographically smallest products whose path passes through that node. If the products are inserted in sorted order and each node keeps the first three that reach it, the stored list at any node is already the correct answer for the prefix spelled by the path to that node.
Answering a query then becomes a walk down the Trie. Typing character i moves from the current node to its child for that character. The suggestions for the current prefix are the cached list at that child. Once a character has no matching child, every longer prefix also has no match, so the rest of the answer is empty lists.
This trades extra space and a preprocessing pass for query-time cost that no longer depends on the number of products. It pays off when one product list serves many search words.
products array lexicographically.node pointer from the root. For each character of searchWord:node is not null, follow the child for the current character (which may be null).node is now null, the suggestions for this and all later prefixes are empty.node.Products sorted: ["mobile", "moneypot", "monitor", "mouse", "mousepad"], searchWord = "mouse". Inserting in this order fills each node's cached list with the first three products to pass through it.
m caches ["mobile", "moneypot", "monitor"] (first three of the five m... products).m -> o caches ["mobile", "moneypot", "monitor"] (same first three, all start with mo).mo -> u caches ["mouse", "mousepad"] (only these two pass through mou).mou -> s caches ["mouse", "mousepad"].mous -> e caches ["mouse", "mousepad"].Walking the query:
m: node = child m, suggestions = ["mobile", "moneypot", "monitor"].o: node = mo, suggestions = ["mobile", "moneypot", "monitor"].u: node = mou, suggestions = ["mouse", "mousepad"].s: node = mous, suggestions = ["mouse", "mousepad"].e: node = mouse, suggestions = ["mouse", "mousepad"].