LeetCode-347-前k个高频元素-中等(红黑树/堆/快排)

一 题目

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

二 前言

像这种topK问题(前k小/前k大/第k大/第k小)的题解都是套路,基本就是堆排序O(nlogk)、快速选择O(n)【最快】、二叉搜索树O(nlogk)。本题只是在这基础上的变形而已,要处理值和频率对应的问题,所以考虑以哈希表为底层的unordered_map来处理,然后在后面排序的过程中需要注意数据结构的变化。

三 题解

1.两个红黑树反着赋值。

思路:考虑到c++中的map底层是红黑树实现的,会对key值排序,其遍历结果默认为从小到大,因此可以选择用map实现,利用map对频率进行排序,则最后反向迭代器遍历出的前k个数即为题目所求。myMap1用来统计各个数字出现的频率,再反向赋值给myMap2,因为频率相同的数字可能会重复,所以要用容器来装,然后用反向迭代器遍历结果。

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        map<int,int>myMap1;
        //因为频率相同的数字可能不止一个,所以要用数组来存
        map<int,vector<int>>myMap2;
        // 数字对应频率映射
        for(int n : nums){
            myMap1[n]++;
        }
        // 频率对应数字映射
        for(auto it = myMap1.begin(); it!= myMap1.end(); it++){
            myMap2[it->second].push_back(it->first);
        }
        vector<int>ans;
        //考虑到myMap2的迭代器可能指向一个数组,所以不能用下标值的方式去给ans赋值
        for(auto it = myMap2.rbegin(); it != myMap2.rend(); it++){
            ans.insert(ans.end(), (it->second).begin(), (it->second).end());
            if(ans.size() == k)
                break;
        }
        return ans;
    }
};

 2.堆

思路:与普通的topK类型题目一致,可以使用小根堆来维护前k个出现频率大的数,而在此基础上要用哈希表来处理数值与频率的对应关系。哈希表处理完映射关系以后,遍历该表,当堆中元素小于k时直接插入,当剩余k+1个元素则要判断其频率和堆顶的大小关系,如果遍历到的元素出现频率比堆顶大,堆顶就应该被弹出,否则就跳过(←所以要写成两个if语句,不能合并写。

class Solution {
public:
    class cmp{
    public:
        bool operator()(pair<int,int>&a,pair<int,int>&b){
            return a.second > b.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int>HSTable;
        for(int n : nums){
            HSTable[n]++;
        }
        priority_queue<pair<int,int>,vector<pair<int,int>>,cmp>Q;
        for(auto [num,counts] : HSTable){
            if(Q.size() >= k){
                if(counts > Q.top().second){//不能合并成一个if语句
                    Q.pop();
                    Q.emplace(num,counts);
                }
            }else
                Q.emplace(num,counts);
        }
        vector<int>ans;
        while(!Q.empty()){
            ans.emplace_back(Q.top().first);
            Q.pop();
        }
        return ans;
    }
};

注:

1.emplace、emplace_back、emplace_front分别对应insert、push_back、push_front,是C++11增加的新特性,相比push_back更为优化,不用移动或拷贝内存。

2.有关增强型循环的理解:

for(auto a : b) 用a遍历容器b获取其每一个值,但是不能通过修改a来修改b容器中的元素。

for(auto &a : b)传引用的方式,使得可以通过a对容器b内的元素进行更改。

3.重载 < 运算符

C++中的sort()是单调递增的,也就是本来是 < ,但如果把小于号重载成大于号,那么元素就是单调递减了,而c++中的堆就相当于sort数组完以后,把最后一个元素当作堆顶。因此重载成大于号以后,本来C++默认底层是大根堆的优先队列就会变成小根堆。

4.仿函数

仿函数是使一个类的使用看上去像一个函数,实际上就是在类中实现了operator(),使这个类具有类似函数的行为,所以是仿函数类。而我们通常写的greater<int>或者less<int>也是仿函数。

关于自定义排序的问题可以具体看:C++中的仿函数functor_我的大学-CSDN博客_c++仿函数

 3.快速选择

思路也差不多,频率用哈希表处理,分治地选择其中一边来快排。想要实现升序或降序只用更改partition()里两个while循环对cur[r].second和pivot之间大于或小于的关系。

class Solution {
public:
    void randomParttition(vector<pair<int,int> >& cur, int l, int r){
        int pivot = rand() % (r-l+1) + l;
        swap(cur[l],cur[pivot]);
    }
    int partition(vector<pair<int,int> >& cur, int l, int r){
        randomParttition(cur,l, r);
        int pivot = cur[l].second;
        int value = cur[l].first;
        while(l < r){
            while(l < r && cur[r].second <= pivot) --r;
            cur[l] = cur[r];
            while(l < r && cur[l].second >= pivot) ++l;
            cur[r] = cur[l];
        }
        cur[l].first = value;
        cur[l].second = pivot;
        return l;
    }
    void quickSort(vector<pair<int,int> >& cur, int l, int r, int k){
        if(l >= r)  return;
        int pivot = partition(cur,l,r);
        if(pivot == k)  return;
        if(pivot < k)
            quickSort(cur,pivot+1,r,k);
        if(pivot > k)
            quickSort(cur,l,pivot-1,k);
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int>HSTable;
        // 哈希表存入数字对应频率的映射
        for(int i : nums){
            HSTable[i]++;
        }
        vector<pair<int,int> >cur;
        // 初始化待快排的数组
        for(auto j : HSTable){
            cur.emplace_back(j);
        }
        quickSort(cur,0,cur.size()-1,k-1);
        vector<int>ans(k);
        for(int i = 0; i < k; i++){
            ans[i] = cur[i].first;
        }
        return ans;
    }
};

4.二叉搜索树

思路基本跟小根堆那个做法是一样的,先用哈希表统计频率,然后建造一个大小为k的二叉搜索树来维护前k个频率大的数。

### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值