【中等】347. 前 K 个高频元素

0 阅读4分钟

给你一个整数数组 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 **是数组大小。


1. 生活案例:人气练习生选拔赛

想象你是一个选秀节目的制片人,你需要从 100 个练习生(数字)中选出 人气最高(出现频率最高)KK 个人。

  • 第一步:计票(统计频率)

    你让工作人员统计每个练习生得了多少票。结果记录在笔记本上(Map):练习生 A 得了 10 票,练习生 B 得了 3 票……

  • 第二步:搭建一个“底线舞台”(小顶堆)

    你只准备了 KK 个座位的舞台。规则很残酷:如果你想上台,你的票数必须比台上那个“票数最少的人”还要多。

    • 一旦台上坐满了 KK 个人,新来的人如果票数更多,就把那个票数最少的踢下去,自己坐上去。
    • 这个舞台的特点是:票数最少的人永远坐在舞台最显眼的位置(堆顶) ,方便随时被踢走。
  • 第三步:结果公示

    最后留在舞台上的这 KK 个人,就是人气最高的前 KK 名。


2. 代码解析与“生活化”注释

这段代码展示了如何手动实现一个“小顶堆”来维护这前 KK 个高频元素。

JavaScript

var topKFrequent = function(nums, k) {
    // 1. 计票:使用 Map 统计每个数字出现的次数
    let map = new Map();
    for(let num of nums){
        map.set(num, (map.get(num) || 0) + 1);
    }

    // 2. 准备舞台:heap 数组用来模拟“堆”
    let heap = [];

    // 这是一个“向上调整”的过程:新成员入场,根据票数往上爬
    let push = (val) => {
        heap.push(val);
        let cur = heap.length - 1;
        while(cur > 0){
            // 找到他的“长辈”(父节点)
            let parent = Math.floor((cur - 1) / 2);
            // 如果他的票数比长辈还少,他就得跟长辈换位置(小顶堆:小的在上)
            if(heap[cur][1] < heap[parent][1]){
                [heap[cur], heap[parent]] = [heap[parent], heap[cur]];
                cur = parent;
            } else break;
        }
    };

    // 这是一个“向下调整”的过程:淘汰了最弱的人,让剩下的人重新排座次
    let pop = () => {
        let top = heap[0]; // 拿走舞台上票数最少的人
        heap[0] = heap.pop(); // 让队伍最后一个人先顶上去
        let cur = 0;
        while(cur * 2 + 1 < heap.length){
            let left = cur * 2 + 1, right = cur * 2 + 2, smaller = left;
            // 看看两个“后辈”里谁的票数更少
            if(right < heap.length && heap[right][1] < heap[left][1]) smaller = right;
            // 如果顶上来的人票数比后辈多,他得往下挪
            if(heap[smaller][1] < heap[cur][1]){
                [heap[cur], heap[smaller]] = [heap[smaller], heap[cur]];
                cur = smaller;
            } else break;
        }
        return top;
    };

    // 3. 开始比赛
    for (let entry of map.entries()) {
        push(entry); // 尝试让练习生上台
        // 关键逻辑:如果舞台坐不下了(超过K个人),把票数最少(堆顶)的人踢走
        if (heap.length > k) pop();
    }

    // 最后舞台上剩下的就是我们要的人,把他们的名字(数字本身)取出来
    return heap.map(x => x[0]);
};

3. 为什么代码这样写?(核心优势)

  1. 为什么不用全部排序?

    • 如果我们把所有练习生按票数排序,时间复杂度是 O(NlogN)O(N \log N)
    • 使用堆,我们只需要维护一个大小为 KK 的小舞台,复杂度是 O(NlogK)O(N \log K)。当 KK 远小于 NN 时,这种方法快得多!
  2. 小顶堆的妙用

    • 我们要找的是“最高频”,却用“小顶堆”?
    • 生活化理解:因为我们要不断地把“最不给力的人”踢走,所以必须让那个票数最低的人待在最容易操作的位置(堆顶)。

总结

这道题是数据结构面试中的常客。它综合了 哈希表统计堆排序思想

    let n = nums.length;
    let map = new Map();
    for (let num of nums) {
        count = (map.get(num) || 0) + 1;
        map.set(num,count);
    }
    let arr=[...map.entries()];
    arr.sort((a,b)=>b[1]-a[1]);
    return arr.slice(0,k).map(item=>item[0])
};