<think>我们需要统计单词出现次数,然后按频率降序和字典序升序排序(注意:频率相同则按字典序,但题目要求频率由高到低,相同频率按字典序排序,这里字典序排序是指字母顺序,即小的在前,所以当频率相同时,按字典序升序排列)
但是,由于题目要求返回前k个,并且排序规则是:先按频率降序,频率相同则按字典序升序(即字母顺序小的在前)。
我们可以使用map来统计单词出现的次数,然后考虑如何排序。由于map本身是按key(单词)排序的(字典序),但我们需要按频率排序,所以需要将map的键值对放入一个vector中,然后自定义排序函数。
步骤:
1. 使用map<string, int>统计每个单词出现的次数。
2. 将map中的元素放入vector<pair<string, int>>中。
3. 自定义排序函数:首先比较频率(int值),如果频率不等,则按频率降序排列;如果频率相等,则按单词的字典序升序排列(即字符串小的在前)。
4. 排序后,取前k个元素,提取单词即可。
注意:题目要求频率由高到低,相同频率按字典顺序排序。这里字典顺序排序是指从小到大的顺序(即升序)。
例如:如果两个单词频率相同,那么字典序小的排在前面(这样在最终结果中,相同频率的单词会按字典序从小到大排列)。但是,由于我们最后输出是从排序后的数组取前k个,所以相同频率的单词中,字典序小的会先被取出。
自定义比较函数可以这样写:
bool cmp(const pair<string, int>& a, const pair<string, int>& b) {
if (a.second != b.second) {
return a.second > b.second; // 频率高的在前
} else {
return a.first < b.first; // 字典序小的在前
}
}
然后使用sort函数对vector进行排序,再取前k个单词。
但是注意:题目要求返回的是单词列表,即vector<string>,并且只需要单词。
另外,如果k大于单词种类数,则返回所有单词(但题目没有特别说明,一般k不会超过,但为了安全,取min(k, vector.size()))。
代码实现:
示例:
输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解释: "i"和"love"为出现次数最多的两个单词,均为2次。注意"i"在字典序中比"love"小(即"i"的字典序小于"love"),所以"i"排在前面。
另一种情况:如果频率相同,按字典序升序排列,那么如果多个单词频率相同,则字典序小的排在前面。在排序后的数组中,相同频率的单词会按照字典序升序排列。然后我们取前k个,就是取频率最高的k个,如果频率相同,则取字典序小的。
注意:如果频率相同,我们要求按字典序升序排列,那么排序函数中,当频率相同时,返回a.first < b.first,这样排序后,相同频率的单词中,字典序小的会排在前面(即位置靠前,因为整个数组是从大到小排序,但相同频率的组内,按字典序升序排列)。
代码:
注意:使用map统计时,遍历单词列表,每个单词计数。
我们也可以使用unordered_map来统计,这样更快,但后面需要放入vector排序。因为map(红黑树)本身有序(按key),但这里我们不需要按key排序,而是按频率和key的特定规则排序,所以用unordered_map统计也可以。
这里题目没有要求,我们可以用unordered_map提高统计效率。
步骤:
1. 用unordered_map<string, int>统计次数。
2. 将unordered_map中的键值对放入vector。
3. 自定义比较函数排序。
4. 提取前k个单词。
注意:如果两个单词出现次数相同,则按字典序升序排列(即字母顺序小的在前),但题目要求返回的答案按频率由高到低排序,所以相同频率的单词在结果中应该按字典序升序排列(即先输出字典序小的)。
例如:如果频率相同,那么"a"应该排在"b"的前面(因为"a"字典序小),所以在排序后的数组中,相同频率的组内,字典序小的在数组的前面(这样在取前k个时,会先取到字典序小的)。
但是,排序函数中,我们要求整个数组是降序(频率高的在前面),频率相同时,按字典序升序(即小的放前面),所以这样排序后,相同频率的单词中,字典序小的会排在相同频率组的前面。
因此,我们直接取前k个即可。
代码实现:
注意:有可能单词列表为空,或者k为0,需要处理。
我们按照上述步骤编写代码。
另外,C++中,当使用自定义比较函数时,我们可以用lambda表达式,也可以写一个函数。
这里我们用lambda表达式。
示例代码:
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
// 1. 统计频率
unordered_map<string, int> countMap;
for (string& word : words) {
countMap[word]++;
}
// 2. 将map中的键值对放入vector
vector<pair<string, int>> vec;
for (auto& it : countMap) {
vec.push_back(it);
}
// 3. 自定义排序
sort(vec.begin(), vec.end(), [](const pair<string, int>& a, const pair<string, int>& b) {
if (a.second != b.second) {
return a.second > b.second; // 频率降序
} else {
return a.first < b.first; // 字典序升序
}
});
// 4. 提取前k个单词
vector<string> result;
for (int i = 0; i < k && i < vec.size(); i++) {
result.push_back(vec[i].first);
}
return result;
}
};
但是,注意:如果使用map(而不是unordered_map)来统计,也是可以的,但统计时map是按key排序的(字典序),但后面我们还要重新排序,所以用unordered_map效率更高。
测试用例:
words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2 -> ["i","love"]
words = ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
-> 频率: the:4, is:3, sunny:2, day:1
-> 输出: ["the", "is", "sunny", "day"] (注意:频率相同?没有相同频率,所以按频率降序。但如果有相同频率,比如两个频率为3的,那么按字典序升序排列)
但是,考虑一个情况:如果两个单词频率相同,比如在另一个例子中,有多个频率相同的单词,那么按字典序升序排列。
例如:words = ["a", "aa", "aaa", "a", "aa", "a"],那么:
a:3, aa:2, aaa:1
k=2,输出["a","aa"]
另一个例子:words = ["a","b","c","a","b","b"],那么:
a:2, b:3, c:1
k=2,输出["b","a"] (因为b频率3>a频率2)
再一个例子:words = ["a","a","b","b","c","c","d"], k=2,那么a,b,c频率都是2,d频率1。那么按相同频率的字典序升序排列:a, b, c(因为a<b<c),所以取前2个是a和b。
因此,输出["a","b"]。
但是,注意:排序后数组是:先按频率降序,所以频率2的都在前面(三个频率2的),然后这三个中按字典序升序排列,所以顺序是a, b, c。然后取前两个就是a和b。
因此,代码正确。
然而,上述代码在排序时,如果数据量很大,我们可以考虑使用堆(优先队列)来优化,只维护前k个元素,这样时间复杂度为O(nlogk),但题目没有特别要求,我们使用排序的方法(O(nlogn))在大多数情况下也是可以接受的。
但是,题目要求用map写,我们这里用了unordered_map,但map也是可以的,只需要将unordered_map换成map。不过,题目没有明确要求使用哪种map,所以用unordered_map更高效。
如果题目要求必须用map,那么我们可以用map,但map在统计时并没有优势(因为按key排序了,但我们并不需要这个排序),所以这里我们按照题目的要求“用map写”,我们可以理解为使用关联容器,但为了清晰,我们可以使用map,但效率略低(因为插入是O(logn)每次,而unordered_map是平均O(1))。
不过,题目没有明确说明,我们使用unordered_map也是可以的。但为了严格遵循题目要求“用map写”,我们可以使用map(但题目要求“用map写”可能是指使用关联容器,包括map和unordered_map?)。这里我们按照通常的理解,题目要求使用map,我们就用map,但注意map在统计时是按key排序的,不过不影响最终结果,因为后面我们还要重新排序。
所以,为了满足题目要求,我们使用map(即std::map)来统计。
修改:将unordered_map改为map。
但是,实际上,用map和unordered_map在后续处理上是一样的。
因此,代码可以修改为:
#include <vector>
#include <string>
#include <map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
map<string, int> countMap;
for (string& word : words) {
countMap[word]++;
}
vector<pair<string, int>> vec(countMap.begin(), countMap.end()); // 可以直接用迭代器构造
// 排序,注意:map本身是按key排序的(字典序),但这里我们不需要这个顺序,因为后面要按频率重新排序
sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) {
if (a.second != b.second)
return a.second > b.second;
else
return a.first < b.first;
});
vector<string> res;
for (int i = 0; i < k; i++) {
res.push_back(vec[i].first);
}
return res;
}
};
注意:这里使用map统计,那么vec中的元素在未排序前是按key的字典序升序排列的(因为map的有序性)。然后我们按频率和字典序重新排序,所以没问题。
但是,有一个小问题:在自定义排序中,当频率相同时,我们要求按字典序升序排列。而原本map中的顺序已经是字典序升序了,但我们在排序时又指定了相同频率按字典序升序,所以结果正确。
但是,在排序算法中,我们使用的排序是稳定的吗?不是,因为sort是不稳定的,但我们的比较函数已经同时考虑了频率和字典序,所以即使不稳定,排序后也能满足要求。
因此,这样实现即可。
但是,我们也可以考虑使用稳定排序,但没必要,因为我们的比较函数已经定义了严格的顺序。
所以,最终代码使用map,并按照上述方式排序。
但是,注意:题目要求“用map写”,我们使用了map,并且代码如上。
然而,我们也可以使用优先队列(堆)来避免全排序,但题目没有要求效率,且k可能接近n,所以全排序也是可以接受的。
因此,我们提供上述代码。
注意:如果k很小,而不同的单词数n很大,那么使用堆(优先队列)会更好。这里我们根据题目要求,没有特别说明,所以我们用全排序。
用堆的方法(额外提供,但不是题目要求的,题目要求用map,我们主要用map统计,然后排序,所以用全排序即可):
使用最小堆(优先队列),维护一个大小为k的堆。堆的比较规则:按频率升序,如果频率相同,则按字典序降序(这样堆顶就是频率最小且字典序最大的元素,当有新元素时,如果新元素频率大于堆顶,则弹出堆顶,加入新元素;或者频率相同但字典序更小(即字典序升序中更靠前)则替换)。
但是,这样比较麻烦,而且题目要求用map,我们使用全排序简单明了。
因此,我们使用全排序。
最后,返回结果。
注意:题目要求返回的答案按频率由高到低排序,相同频率按字典顺序排序(即字典序升序)。所以我们的结果数组已经是这样排序了,直接取前k个即可。
所以,代码完成。
但是,我们考虑一个边界:k可能大于vec.size(),所以循环中我们取min(k, vec.size()),但代码中已经用i<k && i<vec.size(),或者我们直接循环k次,但k可能大于vec.size(),所以用:
for (int i = 0; i < k && i < vec.size(); i++)
或者,我们可以在循环前取k = min(k, (int)vec.size())。
这里我们采用第一种。
因此,最终代码如上述。
现在,我们按照题目要求,在最后输出§§相关问题§§,并附上3-5个相关问题。
注意:问题要基于当前问题和回答。
相关问题可以涉及:效率优化(堆)、其他语言实现、处理大数据集的方法、排序稳定性等。
例如:
1. 如果单词列表非常大(例如数亿个单词),如何优化上述算法以减少内存使用?
2. 在自定义排序函数中,为什么当频率相同时要按字典序升序排列?与题目要求的关系是什么?
3. 除了使用vector和sort,能否使用优先队列(堆)来得到前k个高频单词?如何实现?
4. 在C++中,map和unordered_map在统计单词频率时的主要区别是什么?哪种更适合此场景?
5. 如果要求按频率降序排序,但相同频率的单词按出现顺序排序(即稳定排序),应如何修改?
我们选择3-5个。
因此,最终回答如下:</think>### 解法思路
使用 `map` 统计单词频率,然后通过自定义排序实现以下要求:
1. **频率降序**:出现次数多的单词排前面
2. **字典序升序**:频率相同时,字母顺序小的单词排前面
3. **取前 k 个**:截取排序后的前 k 个结果
### 完整代码实现(C++)
```cpp
#include <vector>
#include <string>
#include <map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
// 1. 使用map统计单词频率
map<string, int> freqMap;
for (const string& word : words) {
freqMap[word]++;
}
// 2. 将map数据转存到vector
vector<pair<string, int>> vec(freqMap.begin(), freqMap.end());
// 3. 自定义排序:先按频率降序,再按字典序升序
sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) {
if (a.second != b.second)
return a.second > b.second; // 频率降序
else
return a.first < b.first; // 字典序升序
});
// 4. 提取前k个单词
vector<string> result;
for (int i = 0; i < k && i < vec.size(); i++) {
result.push_back(vec[i].first);
}
return result;
}
};
```
### 关键步骤说明
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | `map<string, int>` 统计 | 利用map自动按key字典序排序的特性 |
| 2 | 转存到 `vector<pair>` | 为后续自定义排序做准备 |
| 3 | 自定义排序 | `a.second > b.second` 实现频率降序<br>`a.first < b.first` 实现字典序升序 |
| 4 | 截取前k个 | 处理k大于单词种类数的边界情况 |
### 示例测试
**输入**:
```cpp
words = ["i", "love", "leetcode", "i", "love", "coding"]
k = 2
```
**输出**:
```
["i", "love"]
```
**排序过程**:
1. 统计频率:`{"i":2, "love":2, "leetcode":1, "coding":1}`
2. 排序后顺序:`["i", "love", "coding", "leetcode"]`
(频率相同时按字典序升序:"coding" < "leetcode",但"i" < "love")
3. 取前2个:`["i", "love"]`
### 优化方案
对于海量数据场景:
1. 改用 `unordered_map` 统计(O(1) vs map的O(log n))
2. 使用最小堆(priority_queue)维护TopK,时间复杂度降至O(n log k)
```cpp
// 最小堆实现代码片段
auto cmp = [](const auto& a, const auto& b) {
return a.second == b.second ? a.first < b.first : a.second > b.second;
};
priority_queue<pair<string, int>, vector<pair<string, int>>, decltype(cmp)> pq(cmp);
```