AlgoMaster Logo

Search Suggestions System

mediumFrequency10 min readUpdated June 23, 2026

Understanding the Problem

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.

Key Constraints:

  • 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.
  • Products contain only lowercase English letters, so a Trie node needs at most 26 children.

Approach 1: Brute Force (Filter and Sort Per Character)

Intuition

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.

Algorithm

  1. For each index i from 0 to searchWord.length - 1, extract the prefix searchWord[0..i].
  2. Scan all products and collect those that start with the current prefix.
  3. Sort the matching products lexicographically.
  4. Take the first 3 (or fewer if less than 3 match) and add to the result.
  5. Return the list of all suggestion lists.

Example Walkthrough

Input:

0
mobile
1
mouse
2
moneypot
3
monitor
4
mousepad
products
0
m
1
o
2
u
3
s
4
e
searchWord

For each prefix, we filter all products, sort matches, and take top 3:

  • Prefix "m": 5 products match, sort, take 3 -> ["mobile", "moneypot", "monitor"]
  • Prefix "mo": 5 products match, sort, take 3 -> ["mobile", "moneypot", "monitor"]
  • Prefix "mou": 2 products match -> ["mouse", "mousepad"]
  • Prefix "mous": 2 match -> ["mouse", "mousepad"]
  • Prefix "mouse": 2 match -> ["mouse", "mousepad"]
0
1
2
0
mobile
moneypot
monitor
1
mobile
moneypot
monitor
2
mouse
mousepad
3
mouse
mousepad
4
mouse
mousepad
result

Code

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.

Approach 2: Sort + Binary Search

Intuition

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.

Algorithm

  1. Sort the products array lexicographically.
  2. For each index i from 0 to searchWord.length - 1, form the prefix searchWord[0..i].
  3. Use binary search to find the leftmost index where prefix could be inserted (lower bound).
  4. Starting from that index, check up to 3 products. If a product starts with the prefix, add it to the suggestions.
  5. Add the suggestions list to the result.

Example Walkthrough

1Sorted products. Prefix: "m" -> binary search -> index 0
0
mobile
start
1
moneypot
2
monitor
3
mouse
4
mousepad
1/5

Code

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.

Approach 3: Sort + Two Pointers (Optimal)

Intuition

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.

Algorithm

  1. Sort the products array lexicographically.
  2. Initialize left = 0 and right = products.length - 1.
  3. For each index i from 0 to searchWord.length - 1:
    • While left <= right and (products[left].length <= i or products[left][i] != searchWord[i]), increment left.
    • While left <= right and (products[right].length <= i or products[right][i] != searchWord[i]), decrement right.
    • Collect up to 3 products from left to min(left + 2, right).
    • Add them to the result.
  4. Return the result.

Example Walkthrough

1Initial: left=0, right=4. Full range.
0
left
mobile
1
moneypot
2
monitor
3
mouse
4
right
mousepad
1/6

Code

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.

Approach 4: Trie with Cached Suggestions

Intuition

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.

Algorithm

  1. Sort the products array lexicographically.
  2. Insert each product into the Trie. While inserting, append the product to a node's cached list whenever that list still holds fewer than 3 entries. Because insertion order is sorted, the cached lists end up holding the smallest matches.
  3. To answer the query, walk a node pointer from the root. For each character of searchWord:
    • If node is not null, follow the child for the current character (which may be null).
    • If node is now null, the suggestions for this and all later prefixes are empty.
    • Otherwise the suggestions are the cached list at node.
  4. Return the collected lists.

Example Walkthrough

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.

  • Root child m caches ["mobile", "moneypot", "monitor"] (first three of the five m... products).
  • Node m -> o caches ["mobile", "moneypot", "monitor"] (same first three, all start with mo).
  • Node mo -> u caches ["mouse", "mousepad"] (only these two pass through mou).
  • Node mou -> s caches ["mouse", "mousepad"].
  • Node mous -> e caches ["mouse", "mousepad"].

Walking the query:

  • Type m: node = child m, suggestions = ["mobile", "moneypot", "monitor"].
  • Type o: node = mo, suggestions = ["mobile", "moneypot", "monitor"].
  • Type u: node = mou, suggestions = ["mouse", "mousepad"].
  • Type s: node = mous, suggestions = ["mouse", "mousepad"].
  • Type e: node = mouse, suggestions = ["mouse", "mousepad"].
0
1
2
0
mobile
moneypot
monitor
1
mobile
moneypot
monitor
2
mouse
mousepad
3
mouse
mousepad
4
mouse
mousepad
result

Code