【LeetCode刷题日记】347.前k个高频元素

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!
摘要:

本文介绍了两种基于堆结构(优先级队列)的算法实现,用于解决"返回数组中出现频率前k高的元素"问题。

方法一使用大顶堆,对所有元素进行排序后取前k个;

方法二采用更高效的小顶堆,仅维护k个元素,通过不断替换堆顶来保留高频元素。

两种方法都先用哈希表统计元素频率,再通过不同堆结构进行排序,最终时间复杂度优于O(nlogn)。小顶堆实现更为优化,仅需O(nlogk)时间复杂度,适合处理大规模数据。

题目背景:347.前k个高频元素(中等)

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

示例 1:

**输入:**nums = [1,1,1,2,2,3], k = 2

输出:[1,2]

示例 2:

**输入:**nums = [1], k = 1

输出:[1]

示例 3:

**输入:**nums = [1,2,1,2,1,2,3,1,3,2], k = 2

输出:[1,2]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶: 你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n是数组大小。

题目分析:

我们拿到这个题目,题目要求是返回出现频率前k高的元素,那我们首先想到的就是要记录给定数组中元素的出现次数,然后我们要返回元素,就不能仅仅记录次数,要记录两个数据,并且这个次数是和元素一一对应的,这里我们能想到map集合,很容易。

记录完次数之后,我们要返回前k个频率高的,也就是说我们要对次数进行排序,然后返回数组中的前k个的元素。对频率进行排序,这里我们可以使用一种 容器适配器就是优先队列。这是整体的思路

关于优先队列:

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

什么是堆呢

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

本题我们就要使用优先级队列来对部分频率进行排序。

为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。

此时要思考一下,是使用小顶堆呢,还是大顶堆?

有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。

那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。

而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢,但其实大顶堆也能实现,我们一起看看吧。

所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

代码实现:

小顶堆实现:

先构建map集合来记录元素出现的频率,循环遍历数组进行添加

复制代码
    map.put(num, map.getOrDefault(num,0) + 1);
  • 查找当前次数map.getOrDefault(num, 0) 会去哈希表里查找 num 已经出现了几次。如果之前没出现过(查不到),就返回默认值 0

  • 次数加一 :把查到的次数(可能是默认的0,也可能是之前的数字)加上 1

  • 存回哈希表map.put(num, ...) 把更新后的次数重新存储到哈希表中,键是 num,值是它最新的出现次数。

然后我们就要用优先级队列对频率进行排序

先构建一个优先队列:

复制代码
       PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
  1. PriorityQueue<int[]> :创建了一个优先队列(堆),队列里每个元素都是一个长度为2的数组 int[],这个数组的第一个位置 pair[0] 存数字本身,第二个位置 pair[1] 存这个数字出现的次数(频率)。

  2. (pair1, pair2) -> pair1[1] - pair2[1] :这是一个比较规则(Comparator),决定了堆的排序方式。

    • 它比较两个数组的第二个元素(即频率)。

    • 如果 pair1[1] - pair2[1] 的结果是负数 ,说明 pair1 的频率更小,pair1 会排在 pair2 前面。

    • 这个规则会让频率小的数组 成为堆顶(最先被取出的元素)。

然后我们进行添加,有两个判断条件,我们不需要添加所有,因为用的是小顶堆,当小于k个的时候直接添加,下面的添加是核心逻辑,大于k时,我们要弹出队头,也就是此时堆中频率最小的元素,把新添加的放进去,然后根据我们的规则继续进行比较排序

复制代码
// 堆内部自动执行:
1. 把新元素放到末尾 → [[2,2], [1,3], [3,1]]
2. 比较 (3,1) 和它的父节点 (2,2):
   调用你的规则:compare([3,1], [2,2]) = 1 - 2 = -1
   结果为负数 → [3,1] 应该排在 [2,2] 前面
   所以交换 → [[3,1], [1,3], [2,2]]
3. 继续比较 (3,1) 和新的父节点(没有)→ 停止
  1. 整体效果

    因为堆顶永远是当前堆里频率最小的那个数字 ,所以当你维护这个堆的大小不超过 k 时,堆里剩下的 k 个元素就是频率最大的前 k 个数字(堆顶是第 k 大的频率)。

简单总结:这行代码创建了一个按频率从小到大排序的小顶堆,用于在遍历过程中随时淘汰频率不够大的数字,最终留下频率最高的 k 个。

然后我们把结果弹出,从小顶堆中取出频率最高的前 k 个元素,并按频率从高到低的顺序存入结果数组

复制代码
// 从小顶堆弹出:先得频率低的,后得频率高的
弹出顺序: [3,1] → [2,2] → [1,3]
          
// 从后往前填:让频率高的在前面
ans数组:   [0]   [1]   [2]
          
i=2: ans[2] = poll() → [3,1]  // 最后弹出得,放最后
i=1: ans[1] = poll() → [2,2]  
i=0: ans[0] = poll() → [1,3]  // 最后弹出高频率,放最前

// 最终结果: ans = [1, 2, 3]  ← 频率从高到低
题目答案:
java 复制代码
    //解法2:基于小顶堆实现
    public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
        //出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) { //小顶堆只需要维持k个元素有序
            if (pq.size() < k) { //小顶堆元素个数小于k个时直接加
                pq.add(new int[]{entry.getKey(), entry.getValue()});
            } else {
                if (entry.getValue() > pq.peek()[1]) { //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                    pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                    pq.add(new int[]{entry.getKey(), entry.getValue()});
                }
            }
        }
        int[] ans = new int[k];
        for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
            ans[i] = pq.poll()[0];
        }
        return ans;
    }
}
用大顶堆实现:

主要就是添加的逻辑有所变化,我们要对所有元素进行排序,这是与小顶堆的最大区别,小顶堆只需要维护k个,因为弹出的自动是不符合的,而大顶堆不能弹出,弹出的是频率最高的,是题目所需要的。

复制代码
堆结构(按频率排序):
        (3, 1)   ← 堆顶(全局最大)
       /      \
   (2, 2)    (1, 3)
题目答案:
java 复制代码
class Solution {
    //解法1:基于大顶堆实现
    public int[] topKFrequent1(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
        for (int num : nums) {
            map.put(num, map.getOrDefault(num,0) + 1);
        }
        //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
        //出现次数按从队头到队尾的顺序是从大到小排,出现次数最多的在队头(相当于大顶堆)
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair2[1] - pair1[1]);
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {//大顶堆需要对所有元素进行排序
            pq.add(new int[]{entry.getKey(), entry.getValue()});
        }
        int[] ans = new int[k];
        for (int i = 0; i < k; i++) { //依次从队头弹出k个,就是出现频率前k高的元素
            ans[i] = pq.poll()[0];
        }
        return ans;
    }

复杂度分析

方法 时间复杂度 空间复杂度
大顶堆 O(n + k log n) O(n)
小顶堆 O(n log k) O(k)
  • 时间: O(n log k),n个元素,每个元素最多push/pop一次,每次O(log k)

  • 空间: O(k),堆只存储k个元素 + 哈希表O(n)

结语:如果对你有帮助,请**点赞,关注,收藏,**你的支持就是我最大的鼓励!

相关推荐
tjl521314_213 小时前
02C++ 静态变量与链接性
java·jvm·c++
七颗糖很甜3 小时前
台风数据免费获取教程
大数据·python·算法
AI科技星3 小时前
《全域数学》第一部·数术本源
算法·机器学习·数学建模·数据挖掘·量子计算
此生决int3 小时前
快速复习之数据结构篇——链表
数据结构·链表
阿Y加油吧3 小时前
二刷 LeetCode:118. 杨辉三角 & 198. 打家劫舍 复盘笔记
笔记·算法·leetcode
深邃-3 小时前
【数据结构与算法】-二叉树(1):树的概念与结构,二叉树的概念与结构
数据结构·算法·链表·二叉树··顺序表
风筝在晴天搁浅3 小时前
手撕归并排序
数据结构·算法·排序算法
摇滚侠3 小时前
Public Key Retrieval is not allowed
java·数据库·mysql
qeen873 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·