题目描述

✅ 1268. 搜索推荐系统

image-20260928224048130

image-20260928224048131

题意分析

用户每输入搜索词的一个字符,就按当前完整前缀给出一组推荐:只考虑以该前缀开头的商品,从中取字典序最小的至多三个,按字典序返回。

匹配要求从商品名开头开始,不是任意位置包含搜索片段。没有匹配时也要为这一步保留空列表,因此外层结果的组数必须等于搜索词长度。商品不因被推荐而消耗,同一商品可以出现在多个前缀的结果中。

解法:排序 + 二分定位前缀

核心思路

[!blue]

先把商品名按字典序排序。拥有同一前缀的字符串会连续排列:只要某个字符串在前缀结束前就出现更小或更大的字符,它就会整体排在这一组的前面或后面,不能夹在匹配组内部。因此找到匹配区间的左端后,最前面的至多三个商品就是需要的推荐。

对于当前前缀 prefix,用二分查找第一个不小于它的商品,记为 start。任何以 prefix 开头的商品都不会小于 prefix,所以更前面的商品一定不匹配。二分采用左闭右开的范围 [left, right):中点不小于前缀就保留左半含中点,否则排除中点及其左侧。

lowerBound 只排除了过小的商品,并没有证明找到的商品真正匹配。它可能只是字典序更大的无关名称,甚至已经等于数组长度,所以收集前仍需检查真实前缀。

从 start 向后读取,匹配就加入本组,直到已有三个结果或遇到第一个不匹配的商品。由于匹配区间连续,一旦离开就不可能在更后面重新进入,可以立即停止本组。

对搜索词的每个前缀重复这一过程,并始终追加一组结果。更长前缀只会缩小候选集合,但无匹配时仍然要保留对应空组。排序会重新排列输入商品数组,商品字符串本身不变。

解题步骤

  1. 按字典序对 products 排序,创建外层结果列表。
  2. 逐次扩展搜索前缀,包含从首字符到当前字符的全部内容。
  3. 二分找到第一个不小于当前前缀的商品下标。
  4. 从该位置开始,检查是否以当前前缀开头,按顺序收集至多三个商品;首个不匹配项出现时停止。
  5. 把本轮结果加入外层列表,即使它为空也保留。
  6. 全部字符处理完后返回各个前缀对应的推荐列表。

代码实现

class Solution {
    public List<List<String>> suggestedProducts(String[] products, String searchWord) {
        Arrays.sort(products);
        List<List<String>> answer = new ArrayList<>();
        StringBuilder prefixBuilder = new StringBuilder();

        for (int i = 0; i < searchWord.length(); i++) {
            prefixBuilder.append(searchWord.charAt(i));
            String prefix = prefixBuilder.toString();
            // 二分只提供可能的匹配起点,仍需检查真实前缀。
            int start = lowerBound(products, prefix);

            List<String> cur = new ArrayList<>();

            for (int j = start; j < products.length && cur.size() < 3; j++) {
                // 匹配段连续,第一项不匹配后即可停止本组。
                if (!products[j].startsWith(prefix)) {
                    break;
                }

                cur.add(products[j]);
            }

            // 即使没有推荐,也保留这个输入前缀对应的空组。
            answer.add(cur);
        }

        return answer;
    }

    private int lowerBound(String[] products, String prefix) {
        int left = 0;
        int right = products.length;

        while (left < right) {
            int mid = left + (right - left) / 2;

            if (products[mid].compareTo(prefix) >= 0) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return left;
    }
}
import "sort"

func suggestedProducts(products []string, searchWord string) [][]string {
    sort.Strings(products)
    answer := make([][]string, 0, len(searchWord))

    for i := 0; i < len(searchWord); i++ {
        prefix := searchWord[:i+1]
        // 二分只提供可能的匹配起点,仍需检查真实前缀。
        start := lowerBoundString(products, prefix)

        cur := make([]string, 0, 3)
        for j := start; j < len(products) && len(cur) < 3; j++ {
            // 匹配段连续,第一项不匹配后即可停止本组。
            if !hasPrefix(products[j], prefix) {
                break
            }
            cur = append(cur, products[j])
        }
        // 即使没有推荐,也保留这个输入前缀对应的空组。
        answer = append(answer, cur)
    }

    return answer
}

func lowerBoundString(products []string, prefix string) int {
    left, right := 0, len(products)
    for left < right {
        mid := left + (right-left)/2
        if products[mid] >= prefix {
            right = mid
        } else {
            left = mid + 1
        }
    }

    return left
}

func hasPrefix(word string, prefix string) bool {
    return len(word) >= len(prefix) && word[:len(prefix)] == prefix
}

复杂度分析

  • 时间复杂度:O(NL log(N + 1) + M² log(N + 1)),其中 N 为商品数,L 为最长商品长度,M 为搜索词长度。排序比较最多读取 L 个字符;长度为 p 的前缀查询需 O(p log(N + 1)),累加所有前缀长度得到二次项,也涵盖 Java 创建前缀副本的开销。
  • 空间复杂度:不计输出,Java 排序与前缀缓冲为 O(N + M),Go 排序栈为 O(log(N + 1))。每轮结果最多保存三个商品引用,总输出为 O(M)。

关键点总结

[!green]

  • 字典序排序使同前缀商品形成连续区间,并直接决定推荐次序。
  • 二分定位的是可能起点,前缀校验负责确认匹配,两者不能互相替代。
  • 从匹配区间左端取前三个,既满足上限也保证字典序最小。
  • 外层每个位置对应一次输入,不因无匹配而省略结果组。

易错点总结

[!yellow]

  • 使用子串包含判断:商品必须以前缀开头,出现在中间不算匹配。
  • 认为二分结果一定匹配:第一个不小于前缀的商品仍可能无关,需要再次检查。
  • 二分返回数组长度后仍访问商品:这表示没有候选,当前组应为空。
  • 找到全部匹配都加入结果:每组最多三个,只保留字典序最小的前三个。
  • 没有匹配就不追加当前组:会使后续结果与输入字符的位置错位。
  • 忽略字符串比较开销:比较和前缀检查需要逐字符进行,不能无条件当成常数时间。

相似题目

题目 难度 关联与区别
208. 实现 Trie (前缀树) 中等 Trie可定位每个查询前缀,本题还需返回该前缀下字典序最小的至多三个产品。
745. 前缀和后缀搜索 困难 原题同时筛前后缀并按最大下标选择,本题只筛前缀并按字典序选择。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/60401575
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!