在问 · 算法题解

1268. 搜索推荐系统

中等二分查找排序 + 二分查找力扣原题 ↗

题目描述

输入字符串数组 products 和字符串 searchWord。

用户依次键入 searchWord 的每个字母。每键入一个字母后,设当前已键入的部分为前缀,从 products 中找出以这个前缀开头的产品,推荐其中字典序最小的至多 3 个,按字典序排列。没有匹配时推荐列表为空。

返回一个二维列表,第 i 项是键入前 i + 1 个字母后的推荐列表。

示例 1
输入:products = ["mobile","mouse","moneypot","monitor","mousepad"]
searchWord = "mouse"
输出:[["mobile","moneypot","monitor"],["mobile","moneypot","monitor"],["mouse","mousepad"],["mouse","mousepad"],["mouse","mousepad"]]

解释:排序后键入 m、mo 时推荐前三个;键入 mou 及以后只有 mouse 和 mousepad 匹配。

示例 2
输入:products = ["havana"], searchWord = "havana"
输出:[["havana"],["havana"],["havana"],["havana"],["havana"],["havana"]]

解释:每个前缀都只匹配 havana。

数据范围

  • 1 <= products.length <= 1000
  • 1 <= Σ products[i].length <= 2 * 10^4
  • products[i] 只含小写英文字母。
  • 1 <= searchWord.length <= 1000
  • searchWord 只含小写英文字母。

解题思路:排序 + 二分查找

先把 products 按字典序排序,以同一前缀开头的单词在排序后是连续的一段,而且这一段从“第一个 ≥ 前缀的单词”开始。所以每个前缀只需二分查找 lower_bound 位置 lo,然后看 products[lo..lo+2] 中哪些真的以前缀开头。

为什么正确

二分时 products[0..lo-1] 都 < prefix,products[hi..] 都 ≥ prefix;结束时 lo 是第一个 ≥ prefix 的位置。

复杂度分析

排序 O(n log n · L),每个前缀二分 O(log n · L),总计 O((n + m) log n · L);空间 O(1)(不计排序和输出)。

易错点

lo 之后的 3 个单词不一定都以前缀开头,必须逐个检查;lo + 3 可能越界。没有任何匹配时也要放一个空列表。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def suggestedProducts(self, products, searchWord):
        products.sort()
        res, prefix = [], ""
        for c in searchWord:
            prefix += c
            lo, hi = 0, len(products)
            while lo < hi:
                mid = (lo + hi) // 2
                if products[mid] < prefix:
                    lo = mid + 1
                else:
                    hi = mid
            res.append([w for w in products[lo:lo + 3] if w.startswith(prefix)])
        return res

载入编辑器,自己跑一遍 →

其他解法

二分方法利用“排序后共享前缀的字符串连续排列”,每个前缀二分一次即可,不需要额外结构;字典树在每个前缀结点预先存好前 3 个结果,查询最快但占用更多空间。

解法 2:排序 + 字典树 时间 O(n log n · L + 字符总数),空间 O(字符总数)

先把产品按字典序排序,再依次插入字典树。插入时在路径上的每个结点记录至多 3 个产品:因为产品已经有序,先到达的 3 个就是这个前缀下字典序最小的 3 个。查询时沿着 searchWord 一个字符一个字符往下走,每走一步输出当前结点记录的列表;一旦走到不存在的分支,后面全部输出空列表。

每输入一个字符只需往下走一步,贴近真实搜索框的实时联想,也能预先建好后服务大量查询。建树和存储的开销比二分大,只查一个词时并不划算。

class Solution:
    def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]:
        products.sort()
        children = [{}]
        top = [[]]
        for w in products:
            node = 0
            for c in w:
                if c not in children[node]:
                    children[node][c] = len(children)
                    children.append({})
                    top.append([])
                node = children[node][c]
                if len(top[node]) < 3:
                    top[node].append(w)
        res = []
        node = 0
        for c in searchWord:
            node = children[node].get(c, -1) if node >= 0 else -1
            res.append(top[node] if node >= 0 else [])
        return res

二分查找 专题的其他题目

查看 二分查找 全部题目 →