题目描述
这是 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"
输出:[[],[],[],[],[],[],[]]
提示:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = p r o d u c t s . l e n g t h < = 1000 1 <= products.length <= 1000 </math>1<=products.length<=1000
- <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = Σ p r o d u c t s [ i ] . l e n g t h < = 2 × 1 0 4 1 <= Σ products[i].length <= 2 \times 10^4 </math>1<=Σproducts[i].length<=2×104
products[i]
中所有的字符都是小写英文字母。- <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = s e a r c h W o r d . l e n g t h < = 1000 1 <= searchWord.length <= 1000 </math>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
进行排序复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n log n ) O(n\log{n}) </math>O(nlogn);构建字典树复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( ∑ i = 0 n − 1 p s [ i ] . l e n g t h ) O(\sum_{i = 0}^{n - 1}ps[i].length) </math>O(∑i=0n−1ps[i].length);根据w
构建答案复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m 2 ) O(m^2) </math>O(m2);整体复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( ∑ i = 0 n − 1 p s [ i ] . l e n g t h + m 2 ) O(\sum_{i = 0}^{n - 1}ps[i].length + m^2) </math>O(∑i=0n−1ps[i].length+m2) - 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( ∑ i = 0 n − 1 p s [ i ] . l e n g t h × C ) O(\sum_{i = 0}^{n - 1}ps[i].length \times C) </math>O(∑i=0n−1ps[i].length×C),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> C = 26 C = 26 </math>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
进行排序复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n log n ) O(n\log{n}) </math>O(nlogn);每次二分需要进行字符串比较,复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m log n ) O(m\log{n}) </math>O(mlogn);二分到左端点后需要往后检查最多三个字符串,复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m ) O(m) </math>O(m)。整体复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n log n + m log n ) O(n\log{n} + m\log{n}) </math>O(nlogn+mlogn) - 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( log n ) O(\log{n}) </math>O(logn)
最后
这是我们「刷穿 LeetCode」系列文章的第 No.1268
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour... 。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉