[每日一题] 126. 前K个高频单词(字符串、堆排序、桶排序、多方法)

1. 题目来源

链接:前K个高频单词
来源:LeetCode

2. 题目说明

给一非空的单词列表,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。

示例1:

输入: [“i”, “love”, “leetcode”, “i”, “love”, “coding”], k = 2
输出: [“i”, “love”]
解析: “i” 和 “love” 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 “i” 在 “love” 之前。

示例2:

输入: [“the”, “day”, “is”, “sunny”, “the”, “the”, “the”, “sunny”, “is”, “is”], k = 4
输出: [“the”, “is”, “sunny”, “day”]
解析: “the”, “is”, “sunny” 和 “day” 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。

注意:

假定 k 总为有效值, 1 ≤ k ≤ 集合元素数。
输入的单词均由小写字母组成。

扩展练习:

尝试以 O(n log k) 时间复杂度和 O(n) 空间复杂度解决。

3. 题目解析

方法一:最小堆、自定义排序机制解法

这道题让求前K个高频词,跟之前那道题 [每日一题] 127. 前K个高频元素(字符串、堆排序、桶排序、多方法) 极其类似,换了个数据类型就又是一道新题。唯一的不同就是之前那道题对于出现频率相同的数字,没有顺序要求。而这道题对于出现频率相同的单词,需要按照字母顺序来排。但是解法都一样,还是用最小堆和桶排序的方法。

首先来看最小堆的方法:

  • 首先建立每个单词和其出现次数之间的映射,然后把单词和频率的 pair 放进最小堆
  • 如果没有相同频率的单词排序要求,完全可以让频率当作 pair 的第一项,这样 priority_queue 默认是以 pair 的第一项为 key 进行从大到小的排序,而当第一项相等时,又会以第二项由大到小进行排序,这样第一项的排序方式就与题目要求的相同频率的单词要按字母顺序排列不相符
  • 当然也可以在存入结果 res 时对相同频率的词进行重新排序处理,也可以对 priority_queue 的排序机制进行自定义
  • 这里采用第二种方法,自定义排序机制让a.second > b.second,让小频率的词在第一位,然后当 a.second == b.second 时,让 a.first < b.first,这是让字母顺序大的排在前面这里博主需要强调一点的是,priority_queue 的排序机制的写法和 vector 的 sort 的排序机制的写法正好顺序相反,同样的写法,用在 sort 里面就是频率小的在前面,不信的话可以自己试一下)
  • 定义好最小堆后,首先统计单词的出现频率,然后组成 pair 排序最小堆之中,只保存 k 个 pair,超过了就把队首的pair 移除队列,最后把单词放入结果 res 中即可

参见代码如下:

// 执行用时 :28 ms, 在所有 C++ 提交中击败了18.64%的用户
// 内存消耗 :11.4 MB, 在所有 C++ 提交中击败了54.94%的用户

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        vector<string> res(k);
        unordered_map<string, int> freq;
        auto cmp = [](pair<string, int>& a, pair<string, int>& b) {
            return a.second > b.second || (a.second == b.second && a.first < b.first);
        };
        priority_queue<pair<string, int>, vector<pair<string, int>>, decltype(cmp) > q(cmp);
        for (auto word : words) ++freq[word];
        for (auto f : freq) {
            q.push(f);
            if (q.size() > k) q.pop();
        }
        for (int i = res.size() - 1; i >= 0; --i) {
            res[i] = q.top().first; q.pop();
        }
        return res;
    }
};
方法二:利用 set、map 数据结构解法

下面这种解法还是一种堆排序的思路:

  • 首先用map,建立次数和出现该次数所有单词的集合 set 之间的映射,这里也利用了 set 能自动排序的特性
  • 当然还是需要首先建立每个单词和其出现次数的映射,然后将其组成 pair 放入map 种,map 是从小到大排序的
  • 从最后面取pair,就是次数最大的,每次取出一层中所有的单词,如果此时的 K 大于该层的单词个数,就将整层的单词加入结果 res 中,否则就取前 K 个就行了
  • 取完要更更新 K 值,如果 K 小于等于0了,就 break 掉,返回结果 res 即可

参见代码如下:

// 执行用时 :20 ms, 在所有 C++ 提交中击败了71.21%的用户
// 内存消耗 :12.3 MB, 在所有 C++ 提交中击败了5.06%的用户

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        vector<string> res;
        unordered_map<string, int> freq;
        map<int, set<string>> m;
        for (string word : words) ++freq[word];
        for (auto a : freq) {
            m[a.second].insert(a.first);
        }
        for (auto it = m.rbegin(); it != m.rend(); ++it) {
            if (k <= 0) break;
            auto t = it->second;
            vector<string> v(t.begin(), t.end());
            if (k >= t.size()) {
                res.insert(res.end(), v.begin(), v.end());
            } else {
                res.insert(res.end(), v.begin(), v.begin() + k);
            }
            k -= t.size();
        }
        return res;
    }
};
方法三:利用set、map、multiset数据结构,自定义排序机制解法(有坑待填!!)已填

思路基本与方法二一致,代码已经过详细注释:

参见代码如下:

// 执行用时 :24 ms, 在所有 C++ 提交中击败了39.77%的用户
// 内存消耗 :12.1 MB, 在所有 C++ 提交中击败了5.06%的用户

class Solution {
 public:
 
     class Compare {
     public:
         // 在set中进行排序时的比较规则
         bool operator()(const pair<string, int>& left, const pair<string, int>& right) const { // 坑点,为什么去掉const 就直接爆炸了???
             return left.second > right.second;
         }
     };

    vector<string> topKFrequent(vector<string>& words, int k) {
     // 用 <单词,单词出现次数> 构建键值对,然后将vector中的单词放进去,统计每个单词出现的次数
        map<string, int> m;
        for (int i = 0; i < words.size(); ++i) 
            ++(m[words[i]]);
        
        // 将单词按照其出现次数进行排序,出现相同次数的单词集中在一块
        multiset<pair<string, int>, Compare> ms(m.begin(), m.end());
        
        // 将相同次数的单词放在set中,然后再放到vector中   
        set<string> s;
        int count = 0;
        int leftCount = k;

        vector<string> ret;
        for (auto& e : ms) {
            if (!s.empty()) {
                // 相同次数的单词已经全部放到set中
                if (count != e.second) {
                    if (s.size() < leftCount) {
                        ret.insert(ret.end(), s.begin(),s.end());
                        leftCount -= s.size();
                        s.clear();
                    }
                    else {
                        break;
                    }
                }
            }

            count = e.second;
            s.insert(e.first);
        } 

        for (auto& e : s) {
            if (0 == leftCount)
                break;
            
            ret.push_back(e);
            leftCount--;
        }
        return ret;
    }
};

在代码中进行自定义排序方式对 operator() 进行重载的时候,为什么后面不加上const 在 LeetCode 上就会报错呢?查阅了相关资料说是静态断言 的错误,没整明白…此坑待填!!!

原因是const pair& 为 const 对象,仅能调用const 成员函数,所以必须加上 const ,同时取掉 const 后仍会出现错误,猜想是LeetCode 内部实现multiset 时会必须调用 const 类型的自定义比较函数,所以就报这个错误了。

方法四:桶排序法

这种解法是一种桶排序的思路:

  • 根据出现次数建立多个 bucket桶,桶的个数不会超过单词的个数,在每个桶中,对单词按字符顺序进行排序
  • 可以用个数组来表示桶,每一层中放一个集合,利用 set 的自动排序的功能,使其能按字母顺序排列
  • 还是需要首先建立每个单词和其出现次数的映射,然后将其组成 pair 放入map 种,map 是从小到大排序的
  • 这样倒序遍历所有的桶,这样取 pair,就是次数最大的,每次取出一层中所有的单词,如果此时的 k 大于该层的单词个数,就将整层的单词加入结果 res 中,否则就取前 K 个就行了,取完要更更新 K 值,如果 K 小于等于 0 了,就 break 掉,返回结果 res 即可

参见代码如下:

// 执行用时 :20 ms, 在所有 C++ 提交中击败了71.21%的用户
// 内存消耗 :12.8 MB, 在所有 C++ 提交中击败了5.06%的用户

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        vector<string> res;
        unordered_map<string, int> freq;
        vector<set<string>> v(words.size() + 1, set<string>());
        for (string word : words) ++freq[word];
        for (auto a : freq) {
            v[a.second].insert(a.first);
        }
        for (int i = v.size() - 1; i >= 0; --i) {
            if (k <= 0) break;
            vector<string> t(v[i].begin(), v[i].end());
            if (k >= t.size()) {
                res.insert(res.end(), t.begin(), t.end());
            } else {
                res.insert(res.end(), t.begin(), t.begin() + k);
            }
            k -= t.size();
        }
        return res;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值