代码随想录——栈与队列8-前K个高频元素


法一、用数组排序

思路

  1. 用map保存元素和频率关系

  2. 将元素和频率的键值对pair作为vector的基本元素,以频率为准进行从大到小的排序 —— O(nlogn)

  3. 输出前K个pair的first,即数字本身

代码

class Solution {
   public:
       std::vector<int> topKFrequent(std::vector<int>& nums, int k) {
           std::unordered_map<int, int> frequencyMap;
           for (int num : nums) {
               frequencyMap[num]++;
           }
           std::vector<std::pair<int, int>> frequencyPairs;
           for (const auto& pair : frequencyMap) {
               frequencyPairs.push_back(pair);
           }
           std::sort(frequencyPairs.begin(), frequencyPairs.end(), [](const std::pair<int, int>& a, const std::pair<int, int>& b) {
               return a.second > b.second;
           });
           std::vector<int> result;
           for (int i = 0; i < k; ++i) {
               result.push_back(frequencyPairs[i].first);
           }
           return result;
       }
   };

这里用到了lambda表达式定义比较规则。在 C++ 中,可以使用函数指针、函数对象(如类或结构体的重载运算符)或 lambda 表达式来定义比较规则,实现效果是相同的,但语法和使用方式略有不同。(下面的优先队列就是用类定义比较规则)

但题目说明时间复杂度需要优于O(nlogn),而此方法使用的sort函数复杂度等于O(nlogn),因此还不够好。

法二、堆

sort对所有N个元素进行了排序,而题目只要求输出前K个高频元素,那么有没有方法可以只排序K个元素,其他元素不参与排序呢(只与最大/最小值比较,不与其他K-1个元素比较大小),借助堆可以实现。

堆的选择:大根堆 or 小根堆

采用大根堆时,最大值在堆顶,若已有K个元素且需要更新时只能弹出堆顶,这样就失去了最大值。

采用小根堆时,最小值在堆顶,更新堆时弹出的元素一定小于当前堆中所有元素,最后积累的K个元素一定是前K个最大元素。

因此采用小根堆。

优先队列

在 C++ 中,<priority_queue> 是标准模板库(STL)的一部分,用于实现优先队列。

优先队列是一种特殊的队列,它允许我们快速访问队列中具有最高(或最低)优先级的元素。

在 C++ 中,priority_queue 默认是一个最大堆,这意味着队列的顶部元素总是具有最大的值。

priority_queue 是一个容器适配器,它提供了对底层容器的堆操作。它不提供迭代器,也不支持随机访问。

引用自菜鸟教程

从这里可以看出,在C++可以使用优先队列实现堆(或者说优先队列是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列),且在参数缺省时默认构造大根堆。实现小根堆则要更改比较规则,这里提供两种语法。

1. 静态函数

	static bool cmp(pair<int,int>& m,pair<int,int>& n){
			return m.second > n.second; //堆的实现就是n更小于是让n作为父节点——维护最小堆
	}
	priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(&cmp)> q(cmp);
  1. priority_queue
    这里创建了一个优先队列 q,其元素类型为 pair<int, int>
    vector<pair<int, int>>是用来存储优先队列元素的底层容器
    decltype(&cmp) 是一个类型推导,表示 cmp 函数的类型。decltype 用于获取 cmp 的类型,以便在创建优先队列时使用。

  2. 为何return m.second > n.second表示小根堆
    STL的比较器默认实现是std::less<T>,即等价为return a<b,其在sort中体现为元素从小到大排序,但在堆中却对应着堆顶最大的大根堆。这是堆的底层实现导致的:

    priority_queue 的堆构建过程依赖于比较函数来决定哪个元素应该在堆的顶部。默认的比较是 std::less,即 a < b。那么:当 a < b 时,意味着 b 比 a 更大,所以在堆的构建过程中,b 应该排在 a 的上面,让较大的元素更靠近堆顶。因此,堆顶的元素是最大的,构成了一个大顶堆。

    因此,return m.second > n.second自然是n排在m的上面,形成小根堆

    以上内容是询问GPT所得,方便理解,具体STL源码本人还未查阅,希望各位批评指正

2. 类

    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };
    priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
  1. 为什么不需要decltype
    使用类的方式时,编译器已经知道这个类的类型,因此不需要使用 decltype。

思路

  1. 用map统计并保存数字和频率关系
  2. 定义小根堆,基本元素同样是数字和频率的键值对pair
  3. 遍历map,堆中元素不足k个时直接入堆,否则判断当前pair的频次是否大于堆顶,大于则堆顶弹出,当前pair入堆;小于则不做处理
  4. 输出堆中所有pair的key值-数字

代码

class Solution {
public:
    static bool cmp(pair<int,int>& m,pair<int,int>& n){
        return m.second > n.second; //堆的实现就是n更小于是让n作为父节点——维护最小堆
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
        //统计元素出现频率
        unordered_map<int,int> occur;
        for(auto& v : nums){
            occur[v]++;
        }
        //建立小根堆——只能暂时死记了
        priority_queue<pair<int,int>,vector<pair<int,int>>,decltype(&cmp)> q(cmp);  

        for(auto& [num,count] : occur){
            if(q.size() == k){
                if(q.top().second < count){
                    q.pop();
                    q.emplace(num,count);
                }
            }else{
                q.emplace(num,count);
            }
        }
        vector<int> ans;
        while(!q.empty()){
            ans.push_back(q.top().first);
            q.pop();
        }
        return ans;
    }
};

emplace_back 和push_back

这里使用q.push(num,count)报错

push:对于像std::priority_queue这种容器适配器(底层通常是堆结构),push操作主要用于向容器中添加一个新元素。当使用push添加元素时,需要先构造好要添加的元素,然后将其复制(或移动)到容器内部的存储结构中

emplace:emplace是 C++ 11 引入的一个函数,它的主要作用是在容器中直接原位构造(in - place construction)一个新元素,而不是先构造然后再复制(或移动)。这在性能上可能会有优势,特别是当元素的构造函数比较复杂或者涉及资源分配时。

因此如要使用push则应改为q.push({num,count});

posted @ 2024-10-30 11:13  NeroMegumi  阅读(16)  评论(0编辑  收藏  举报