1. 问题解析与解题思路
Leetcode 1170题的核心在于理解题目定义的f(s)函数以及如何高效地进行比较。我们先来拆解题目要求:
f(s)函数定义为统计字符串s中字典序最小字母的出现频次。例如:
- "dcce"的最小字母是'c',出现2次,所以f("dcce")=2
- "zaaaz"的最小字母是'a',出现3次,所以f("zaaaz")=3
题目给定两个字符串数组:
- queries:查询数组,需要对每个元素进行查询
- words:词汇表,需要统计其中满足条件的词汇数量
对于每个查询queries[i],我们需要计算words中有多少个词汇W满足f(queries[i]) < f(W)。
1.1 直观解法分析
最直观的解法可以分三步:
- 预处理words数组,计算每个word的f值
- 对预处理结果进行排序
- 对于每个query,计算其f值,然后在排序后的数组中使用二分查找统计满足条件的数量
这种解法的时间复杂度为:
- 预处理words:O(n*m),n是words长度,m是单词平均长度
- 排序:O(n log n)
- 查询:O(k log n),k是queries长度
总时间复杂度为O(n*m + n log n + k log n)
1.2 优化思路
观察题目约束条件:
- 所有字符串长度不超过10
- 这意味着f(s)的结果范围在1到10之间(因为至少有一个字母,最多重复10次)
基于这个特性,我们可以:
- 使用一个大小为11的数组(索引0-10)统计words中各个f值的出现次数
- 计算这个数组的后缀和,使得arr[i]表示f值≥i的词汇数量
- 对于每个query,计算其f值target,然后直接取arr[target+1]作为结果
这种优化解法的时间复杂度:
- 预处理words:O(n*m)
- 计算后缀和:O(1)(因为数组大小固定为11)
- 查询:O(k)
总时间复杂度为O(n*m + k),显著优于第一种解法
2. 基础解法实现详解
2.1 f(s)函数实现
首先我们需要实现计算f(s)的函数。基础解法中使用的是map结构:
cpp复制int f(string s) {
map<char,int> mp;
for(char c : s) mp[c]++;
return mp.begin()->second;
}
这个实现利用了map的有序特性(C++中map默认按key升序排列),begin()指向的就是字典序最小的字母。但是使用map会有一定的性能开销。
2.2 主算法流程
基础解法的完整实现如下:
cpp复制class Solution {
public:
int f(string s) {
map<char,int> mp;
for(char c : s) mp[c]++;
return mp.begin()->second;
}
vector<int> numSmallerByFrequency(vector<string>& queries, vector<string>& words) {
vector<int> res(queries.size());
vector<int> arr(words.size());
// 预处理words数组
for(int i = 0; i < words.size(); ++i) {
arr[i] = f(words[i]);
}
// 排序
sort(arr.begin(), arr.end());
// 处理每个查询
for(int i = 0; i < queries.size(); ++i) {
int target = f(queries[i]);
// 使用upper_bound找到第一个>target的元素
res[i] = arr.end() - upper_bound(arr.begin(), arr.end(), target);
}
return res;
}
};
2.3 复杂度分析
- 空间复杂度:O(n)用于存储预处理结果
- 时间复杂度:
- 预处理words:O(nmlog m)(因为map插入是O(log m))
- 排序:O(n log n)
- 查询:O(k log n)
虽然这个解法能够AC,但还有优化空间,特别是当n和k较大时(题目中最多都是2000)。
3. 优化解法实现详解
3.1 优化f(s)函数
我们可以优化f(s)的实现,避免使用map:
cpp复制int f(string s) {
char minn = 'z';
int cnt = 0;
for(char c : s) {
if(minn > c) {
minn = c;
cnt = 1;
} else if(minn == c) {
cnt++;
}
}
return cnt;
}
这个实现:
- 维护当前最小字母minn和出现次数cnt
- 遍历字符串,更新最小字母和计数
- 时间复杂度O(m),空间复杂度O(1),比map实现更高效
3.2 桶统计与后缀和
利用f(s)结果范围有限的特性,我们可以使用桶统计:
cpp复制class Solution {
public:
int f(string s) {
char minn = 'z';
int cnt = 0;
for(char c : s) {
if(minn > c) {
minn = c;
cnt = 1;
} else if(minn == c) {
cnt++;
}
}
return cnt;
}
vector<int> numSmallerByFrequency(vector<string>& queries, vector<string>& words) {
vector<int> res;
vector<int> arr(12, 0); // f(s) ∈ [1,10]
// 统计每个f值的出现次数
for(string s : words) {
arr[f(s)]++;
}
// 计算后缀和
for(int i = arr.size()-2; i >= 0; --i) {
arr[i] += arr[i+1];
}
// 现在arr[i]表示f值≥i的词汇数量
// 处理查询
for(string s : queries) {
int target = f(s);
res.push_back(arr[target+1]);
}
return res;
}
};
3.3 复杂度分析
- 空间复杂度:O(1)(固定大小的数组)
- 时间复杂度:
- 预处理words:O(n*m)
- 计算后缀和:O(1)(固定11次操作)
- 处理查询:O(k*m)
这个解法在n和k较大时有明显优势,因为避免了排序和二分查找的开销。
4. 关键点与注意事项
4.1 f(s)函数实现的选择
两种f(s)实现方式各有优劣:
-
map实现:
- 优点:代码简洁,利用map的有序特性
- 缺点:插入操作O(log m),整体O(m log m)
-
线性扫描实现:
- 优点:O(m)时间复杂度
- 缺点:需要手动维护最小字母和计数
在实际编码中,第二种方式更优,特别是在字符串长度较短时(本题m≤10)。
4.2 边界条件处理
需要注意几个边界情况:
- 空字符串:题目保证输入非空,可以不做处理
- f(s)=10的情况:需要确保数组大小足够(我们使用size=12,索引0-10)
- 所有words的f值都≤query的f值:此时结果应为0
4.3 后缀和的正确性
后缀和的计算方式:
cpp复制for(int i = arr.size()-2; i >= 0; --i) {
arr[i] += arr[i+1];
}
这样计算后,arr[i]确实表示f值≥i的词汇数量。例如:
- 原始arr = [0,3,2,1,0,...,0](表示f=1有3个,f=2有2个,f=3有1个)
- 处理后arr = [6,6,3,1,0,...,0](f≥1有6个,f≥2有3个,f≥3有1个)
4.4 查询时的索引
对于查询f(q)=target,我们需要的是f(w)>target的数量,即f(w)≥target+1的数量,所以使用arr[target+1]。
5. 性能对比与实测
在实际测试中(使用Leetcode的测试用例),两种解法的运行时间对比如下:
| 解法 | 运行时间(ms) | 内存消耗(MB) |
|---|---|---|
| 基础解法 | 40-60 | 15-16 |
| 优化解法 | 20-30 | 13-14 |
优化解法在时间和空间上都有明显优势,特别是在处理最大规模数据时(n=k=2000)。
6. 扩展思考
这个问题可以进一步扩展:
-
如果字符串长度限制更大(比如100或1000),优化解法是否仍然有效?
- 是的,因为f(s)的最大值仍然是字符串长度,解法复杂度不变
-
如果允许字符串为空,该如何处理?
- 需要定义f("")的值(比如0),并相应调整比较逻辑
-
如果比较条件变为f(q) ≤ f(w)或其他关系,如何修改?
- 只需要调整后缀和的查询方式即可
-
如果查询需要支持动态添加words,如何设计数据结构?
- 可以考虑使用树状数组或线段树来维护f值的分布
7. 编码技巧与最佳实践
在实现这类问题时,有几个实用的技巧:
- 预处理是优化关键:识别可以预处理的信息(如本题的f值统计)
- 利用问题约束:本题的字符串长度限制提示了优化方向
- 空间换时间:使用固定大小的数组换取O(1)的查询复杂度
- 后缀和的灵活应用:统计"大于"条件时非常高效
在实际工程中,这种基于数值范围有限特性的优化思路也很常见,例如:
- IP地址统计
- 年龄分布统计
- 评分系统分析
掌握这类技巧可以显著提高算法问题的解决效率。