当「华为还是备选,迪爹还是迪子」时宇宙厂一面原题

写在前面

2021 年还是互联网元年,当时常规的华为 Offer 还是普遍人的备选,如今的迪爹(BYD)也还是 "来投就给 Offer" 的迪子。

只有字节,当时是公认炙手可热的"宇宙厂"。

作为在 2021 就提前体验了这两年计算机的"供过于求"的字节,自然在算法题上的难度要超出其他大厂招聘的一档。

那么这道「中国互联网鼎盛时期 中的 鼎盛大厂」的算法笔题,现在的你能做出来吗?

题目描述

这是 LeetCode 上的 1268. 搜索推荐系统 ,难度为 中等。

Tag : 「排序」、「字典树」、「哈希表」、「二分」

给你一个产品数组 products 和一个字符串 searchWord,products 数组中每个产品都是一个字符串。

请你设计一个推荐系统,在依次输入单词 searchWord 的每一个字母后,推荐 products 数组中前缀与 searchWord 相同的最多三个产品。如果前缀相同的可推荐产品超过三个,请按字典序返回最小的三个。

请你以二维列表的形式,返回在输入 searchWord 每个字母后相应的推荐产品的列表。

示例 1:

css 复制代码
输入:products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"
输出:[["mobile","moneypot","monitor"],
["mobile","moneypot","monitor"],
["mouse","mousepad"],
["mouse","mousepad"],
["mouse","mousepad"]
]

解释:按字典序排序后的产品列表是 ["mobile","moneypot","monitor","mouse","mousepad"]
输入 m 和 mo,由于所有产品的前缀都相同,所以系统返回字典序最小的三个产品 ["mobile","moneypot","monitor"]
输入 mou, mous 和 mouse 后系统都返回 ["mouse","mousepad"]

示例 2:

css 复制代码
输入:products = ["havana"], searchWord = "havana"

输出:[["havana"],["havana"],["havana"],["havana"],["havana"],["havana"]]

示例 3:

css 复制代码
输入:products = ["bags","baggage","banner","box","cloths"], searchWord = "bags"

输出:[["baggage","bags","banner"],["baggage","bags","banner"],["baggage","bags"],["bags"]]

示例 4:

ini 复制代码
输入:products = ["havana"], searchWord = "tatiana"

输出:[[],[],[],[],[],[],[]]

提示:

  • 1 < = p r o d u c t s . l e n g t h < = 1000 1 <= products.length <= 1000 1<=products.length<=1000
  • 1 < = Σ p r o d u c t s i . l e n g t h < = 2 × 1 0 4 1 <= Σ productsi.length <= 2 \times 10^4 1<=Σproductsi.length<=2×104
  • products[i] 中所有的字符都是小写英文字母。
  • 1 < = s e a r c h W o r d . l e n g t h < = 1000 1 <= searchWord.length <= 1000 1<=searchWord.length<=1000
  • searchWord 中所有字符都是小写英文字母。

排序 + 字典树 + 哈希表

为了方便,将 products 记为 ps,将 searchWord 记为 w。

这是一个 "Suggestion string" 问题,容易想到字典树进行求解,不了解字典树的同学,可看 前置 🧀。

由于题目要求「若有超过三个的产品可推荐,返回字典序最小的三个」,我们不妨先对 ps 进行排序,使 ps 从前往后满足字典序从小到大。

将所有 ps[i] 按顺序添加到字典树 tr 中,添加过程中,使用两个哈希表 minMap 和 maxMap 分别记录经过某个 tr[i][j] 时的最小 ps 下标和最大 ps 下标 。即哈希表的 key 为具体的 tr[i][j],对应 value 为经过该节点的最小或最大下标。

构建答案时,**根据当前 w 子串到字典树 tr 中查询,定位到该子串对应的 tr[i][j] 为何值,再从哈希表中获取建议字符串在 ps 中的左右端点 l 和 r,**并根据在 ps[l:r] (可能为空集)中最多取三个的原则来构建答案。

考虑实现字典树的两个关键方法,添加 和 查询:

  • 添加函数 void add(String s, int num) :其中 s 为待添加到字典树的字符串,num 则是该字符串在 ps 中的下标编号。

    往字典树添加过程中,按照首次访问字典树节点 tr[i][j] 的下标存入 minMap,最后一次访问字典树节点 tr[i][j] 的下标存入 maxMap 的规则来更新哈希表。

  • 查询函数 int[] query(String s):其中 s 为某个 w 子串,通过查询该子串(最后字符)在字典树的节点值,来得知建议列表对应 ps 的左右端点下标为何值,从而构建答案。

Java 代码:

Java 复制代码
class Solution {
    int[][] tr = new int[20010][26];
    int idx = 0;
    Map<Integer, Integer> min = new HashMap<>(), max = new HashMap<>();
    void add(String s, int num) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) {
                tr[p][u] = ++idx;
                min.put(tr[p][u], num);
            }
            max.put(tr[p][u], num);
            p = tr[p][u];
        }
    }
    int[] query(String s) {
        int a = -1, b = -1, p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) return new int[]{-1, -1};
            a = min.get(tr[p][u]); b = max.get(tr[p][u]);
            p = tr[p][u];
        }
        return new int[]{a, b};
    }
    public List<List<String>> suggestedProducts(String[] ps, String w) {
        Arrays.sort(ps);
        List<List<String>> ans = new ArrayList<>();
        for (int i = 0; i < ps.length; i++) add(ps[i], i);
        for (int i = 0; i < w.length(); i++) {
            List<String> list = new ArrayList<>();
            int[] info = query(w.substring(0, i + 1));
            int l = info[0], r = info[1];
            for (int j = l; j <= Math.min(l + 2, r) && l != -1; j++) list.add(ps[j]);
            ans.add(list);
        }
        return ans;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int tr[20010][26] = {0};
    int idx = 0;
    unordered_map<int, int> minMap, maxMap;
    void add(string s, int num) {
        int p = 0;
        for (char c : s) {
            int u = c - 'a';
            if (tr[p][u] == 0) {
                tr[p][u] = ++idx;
                minMap[tr[p][u]] = num;
            }
            maxMap[tr[p][u]] = num;
            p = tr[p][u];
        }
    }
    pair<int, int> query(string s) {
        int a = -1, b = -1, p = 0;
        for (char c : s) {
            int u = c - 'a';
            if (tr[p][u] == 0) return {-1, -1};
            a = minMap[tr[p][u]];
            b = maxMap[tr[p][u]];
            p = tr[p][u];
        }
        return {a, b};
    }
    vector<vector<string>> suggestedProducts(vector<string>& ps, string w) {
        sort(ps.begin(), ps.end());
        vector<vector<string>> ans;
        for (int i = 0; i < ps.size(); i++) add(ps[i], i);
        for (int i = 0; i < w.length(); i++) {
            vector<string> list;
            pair<int, int> info = query(w.substr(0, i + 1));
            int l = info.first, r = info.second;
            for (int j = l; j <= min(l + 2, r) && l != -1; j++) list.push_back(ps[j]);
            ans.push_back(list);
        }
        return ans;
    }
};

Python 代码:

Python 复制代码
class Solution:
    def suggestedProducts(self, ps: List[str], w: str) -> List[List[str]]:
        tr = defaultdict(lambda: defaultdict(int))
        idx = 0
        minMap, maxMap = {}, {}
        def add(s, num):
            nonlocal idx
            p = 0
            for c in s:
                u = ord(c) - ord('a')
                if tr[p][u] == 0:
                    idx += 1
                    tr[p][u] = idx
                    minMap[tr[p][u]] = num
                maxMap[tr[p][u]] = num
                p = tr[p][u]
        def query(s):
            a, b, p = -1, -1, 0
            for c in s:
                u = ord(c) - ord('a')
                if tr[p][u] == 0:
                    return (-1, -1)
                a = minMap[tr[p][u]]
                b = maxMap[tr[p][u]]
                p = tr[p][u]
            return (a, b)
        ps.sort()
        ans = []
        for i in range(len(ps)):
            add(ps[i], i)
        for i in range(len(w)):
            l, r = query(w[:i + 1])
            lst = [ps[j] for j in range(l, min(l + 3, r + 1)) if l != -1]
            ans.append(lst)
        return ans

TypeScript 代码:

TypeScript 复制代码
let tr: number[][];
let idx: number;
let minMap: Map<number, number>, maxMap: Map<number, number>;
function add(s: string, num: number): void {
    let p = 0;
    for (let i = 0; i < s.length; i++) {
        const u = s.charCodeAt(i) - 'a'.charCodeAt(0);
        if (tr[p][u] === 0) {
            idx++;
            tr[p][u] = idx;
            minMap.set(tr[p][u], num);
        }
        maxMap.set(tr[p][u], num);
        p = tr[p][u];
    }
}
function query(s: string): number[] {
    let a = -1, b = -1, p = 0;
    for (let i = 0; i < s.length; i++) {
        const u = s.charCodeAt(i) - 'a'.charCodeAt(0);
        if (tr[p][u] === 0) return [-1, -1];
        a = minMap.get(tr[p][u])!;
        b = maxMap.get(tr[p][u])!;
        p = tr[p][u];
    }
    return [a, b];
}
function suggestedProducts(ps: string[], w: string): string[][] {
    tr = new Array(20010).fill(0).map(() => new Array(26).fill(0));
    idx = 0;
    minMap = new Map(), maxMap = new Map();
    ps.sort();
    const ans = [];
    for (let i = 0; i < ps.length; i++) add(ps[i], i);
    for (let i = 0; i < w.length; i++) {
        const list = [];
        const [l, r] = query(w.substring(0, i + 1));
        for (let j = l; j <= Math.min(l + 2, r) && l !== -1; j++) list.push(ps[j]);
        ans.push(list);
    }
    return ans;
};
  • 时间复杂度:将 ps 长度记为 n,w 长度记为 m。对 ps 进行排序复杂度为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn);构建字典树复杂度为 O ( ∑ i = 0 n − 1 p s i . l e n g t h ) O(\sum_{i = 0}^{n - 1}psi.length) O(∑i=0n−1psi.length);根据 w 构建答案复杂度为 O ( m 2 ) O(m^2) O(m2);整体复杂度为 O ( ∑ i = 0 n − 1 p s i . l e n g t h + m 2 ) O(\sum_{i = 0}^{n - 1}psi.length + m^2) O(∑i=0n−1psi.length+m2)
  • 空间复杂度: O ( ∑ i = 0 n − 1 p s i . l e n g t h × C ) O(\sum_{i = 0}^{n - 1}psi.length \times C) O(∑i=0n−1psi.length×C),其中 C = 26 C = 26 C=26 为字符集大小

排序 + 二分

由于每个 w 子串只会对应最多三个的建议字符串,同时又可以先通过排序来确保 ps 的有序性。

因此对于每个 w 子串而言,可以 先找到满足要求的,字典序最小的建议字符串 ps[i],接着往后逐个检查,组成最终的建议字符串列表(最多检查三个)。

这个「在 ps 中找符合要求,字典序最小的建议字符串」操作,除了能够利用上述解法来做(省掉一个 maxMap)以外,还能利用字符串本身的有序性进行「二分」,因为该操作本质上,是在找第一个满足 ps[i] 大于等于当前子串的位置。

Java 代码:

Java 复制代码
class Solution {
    public List<List<String>> suggestedProducts(String[] ps, String w) {
        Arrays.sort(ps);
        int n = ps.length;
        List<List<String>> ans = new ArrayList<>();
        for (int i = 0; i < w.length(); i++) {
            String cur = w.substring(0, i + 1);
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (ps[mid].compareTo(cur) >= 0) r = mid;
                else l = mid + 1;
            }
            List<String> list = new ArrayList<>();
            if (ps[r].compareTo(cur) >= 0) {
                for (int j = r; j <= Math.min(n - 1, r + 2); j++) {
                    if (ps[j].length() < cur.length()) break;
                    if (!ps[j].substring(0, i + 1).equals(cur)) break;
                    list.add(ps[j]);
                }
            }
            ans.add(list);
        }
        return ans;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    vector<vector<string>> suggestedProducts(vector<string>& ps, string w) {
        sort(ps.begin(), ps.end());
        int n = ps.size();
        vector<vector<string>> ans;
        for (int i = 0; i < w.length(); i++) {
            string cur = w.substr(0, i + 1);
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = (l + r) >> 1;
                if (ps[mid].compare(cur) >= 0) r = mid;
                else l = mid + 1;
            }
            vector<string> list;
            if (ps[r].compare(cur) >= 0) {
                for (int j = r; j <= min(n - 1, r + 2); j++) {
                    if (ps[j].length() < cur.length()) break;
                    if (ps[j].substr(0, i + 1) != cur) break;
                    list.push_back(ps[j]);
                }
            }
            ans.push_back(list);
        }
        return ans;
    }
};

Python 代码:

Python 复制代码
class Solution:
    def suggestedProducts(self, ps: List[str], w: str) -> List[List[str]]:
        ps.sort()
        n = len(ps)
        ans = []
        for i in range(len(w)):
            cur = w[:i + 1]
            l, r = 0, n - 1
            while l < r:
                mid = (l + r) // 2
                if ps[mid] >= cur:
                    r = mid
                else:
                    l = mid + 1
            lst = []
            if ps[r] >= cur:
                for j in range(r, min(n - 1, r + 2) + 1):
                    if len(ps[j]) < len(cur) or ps[j][:i + 1] != cur:
                        break
                    lst.append(ps[j])
            ans.append(lst)
        return ans

TypeScript 代码:

TypeScript 复制代码
function suggestedProducts(ps: string[], w: string): string[][] {
    ps.sort();
    const n = ps.length;
    const ans = [];
    for (let i = 0; i < w.length; i++) {
        const cur = w.substring(0, i + 1);
        let l = 0, r = n - 1;
        while (l < r) {
            const mid = (l + r) >> 1;
            if (ps[mid].localeCompare(cur) >= 0) r = mid;    
            else l = mid + 1;
        }
        const list: string[] = [];
        if (ps[r].localeCompare(cur) >= 0) {
            for (let j = r; j <= Math.min(n - 1, r + 2); j++) {
                if (ps[j].length < cur.length || !ps[j].startsWith(cur)) break;
                list.push(ps[j]);
            }
        }
        ans.push(list);
    }
    return ans;
};
  • 时间复杂度:将 ps 长度记为 n,w 长度记为 m。对 ps 进行排序复杂度为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn);每次二分需要进行字符串比较,复杂度为 O ( m log ⁡ n ) O(m\log{n}) O(mlogn);二分到左端点后需要往后检查最多三个字符串,复杂度为 O ( m ) O(m) O(m)。整体复杂度为 O ( n log ⁡ n + m log ⁡ n ) O(n\log{n} + m\log{n}) O(nlogn+mlogn)
  • 空间复杂度: O ( log ⁡ n ) O(\log{n}) O(logn)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.1268 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour... 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
Lank_M19 分钟前
白底都占八成,PNG 为何只在图表页赢过 JPG
前端
张一凡9320 分钟前
给 Coding Agent 加一道"架构闸门":Jin 是如何做到"先审架构、再写代码、不漂移"的
前端·后端·agent
Linye20 分钟前
AI 写的 HTML 原型全是空壳?聊聊真实 DOM 交互避坑
前端·人工智能
Jvav练习生29 分钟前
AI Agent 总在关键时刻“失忆”翻车?一文讲透上下文工程(附可运行 Python 代码)
后端
ZANE29 分钟前
纯前端实现的 PDF 与图片互相转换工具技术实现
前端
leo在掘金36 分钟前
只挂了11小时,5000台开发机被塞进SSH公钥
后端·架构
xjz184241 分钟前
Rust 运算符优先级:括号不只用来改变顺序
后端
写码跑山的老艾41 分钟前
为什么有了 MySQL,还要 Redis 和 Elasticsearch?从一件商品看数据系统的权衡
后端
对象存储与RustFS1 小时前
Jenkins 的构建产物别堆在控制器磁盘:用 RustFS 当它的 S3 工件仓库
后端·docker·云原生