(leetcode)力扣100 75前K个高频元素(堆)

题目

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

数据范围

1 <= nums.length <= 105

-104 <= nums[i] <= 104

k 的取值范围是 [1, 数组中不相同的元素的个数]

题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

测试用例

示例1

java 复制代码
输入:nums = [1,1,1,2,2,3], k = 2

输出:[1,2]

示例2

java 复制代码
输入:nums = [1], k = 1

输出:[1]

示例3

java 复制代码
输入:nums = [1,2,1,2,1,2,3,1,3,2], k = 2

输出:[1,2]

题解1(手写堆 时间onlogk,空间on,k为堆大小)

java 复制代码
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        int n = nums.length;
        
        // 【1. 统计频率】:使用哈希表记录每个数字出现的次数
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            // 如果数字之前没出现过,默认次数为0,然后再加1
            map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
        }

        int len = map.size();
        
        // 【2. 转换结构】:创建一个二维数组,行数是不重复数字的个数
        // a[i][0] 存具体的数字,a[i][1] 存该数字出现的频率
        int[][] num = new int[len][2];
        int pos = 0;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            num[pos][0] = entry.getKey();
            num[pos][1] = entry.getValue();
            pos++; // 移动指针,准备存放下一个数字
        }

        // 【3. 建堆】:把刚刚建好的二维数组,整理成一个"大顶堆"(按频率降序排列)
        buildMaxHeap(num, len);
        
        // 准备收集结果的数组
        int[] res = new int[k];
        pos = 0; // 重置 pos,这次用来作为结果数组 res 的下标
        
        // 【4. 提取前 k 个高频元素】:这是堆排序的核心思想!
        // 循环 k 次。使用 map.size() 作为一个固定的常量,防止循环边界随 len 缩小而滑动
        for (int i = len - 1; i > map.size() - 1 - k; i--) {
            // 大顶堆的堆顶(num[0])永远是当前频率最高的元素,直接取它的值放入结果集
            res[pos++] = num[0][0]; 
            
            // 将当前的堆顶(最大值)和堆底元素交换
            swap(0, i, num); 
            
            // 砍掉堆底(因为上一行已经把最大的元素安顿到了末尾),将堆的有效长度减 1
            --len; 
            
            // 此时新的堆顶是一个较小的数,需要让它"下沉",重新调整出一个新的大顶堆
            maxHeapify(num, 0, len); 
        }

        return res;
    }

    // --- 以下是手写大顶堆的底层结构实现 ---

    /**
     * 构建大顶堆
     * 从最后一个非叶子节点 (heapsize/2 - 1) 开始,倒序往上,逐个进行下沉调整
     */
    public void buildMaxHeap(int[][] a, int heapsize) {
        for (int i = heapsize / 2 - 1; i >= 0; i--) {
            maxHeapify(a, i, heapsize);
        }
    }

    /**
     * 堆化操作(下沉 sift-down)
     * 作用:检查当前节点 i,如果它的频率比左右子节点小,就把它和频率最大的子节点交换,并一直往下沉
     */
    public void maxHeapify(int[][] a, int i, int heapsize) {
        int l = i * 2 + 1; // 左子节点的下标
        int r = i * 2 + 2; // 右子节点的下标
        int large = i;     // 默认当前节点 i 是最大的

        // 如果左子节点没有越界,且频率比当前 large 节点的频率大,更新 large
        if (l < heapsize && a[l][1] > a[large][1]) {
            large = l;
        }

        // 如果右子节点没有越界,且频率比当前 large 节点的频率大,更新 large
        if (r < heapsize && a[r][1] > a[large][1]) {
            large = r;
        }

        // 如果发现左右子节点里有比父节点更大的
        if (large != i) {
            // 把频率最大的子节点换到父节点的位置上来
            swap(i, large, a);
            // 交换后,原来的父节点下沉到了 large 的位置,这有可能破坏下面的堆结构
            // 所以需要以 large 为新的起点,继续递归下沉调整!
            maxHeapify(a, large, heapsize);
        }
    }

    /**
     * 交换二维数组中的两个元素(同时交换数字本身和它的频率)
     */
    public void swap(int i, int j, int[][] a) {
        int temp1 = a[i][0];
        int temp2 = a[i][1];
        
        a[i][0] = a[j][0];
        a[i][1] = a[j][1];
        
        a[j][0] = temp1;
        a[j][1] = temp2;
    }
}

题解2(官解1,时空同上,使用优先队列代替手写堆)

java 复制代码
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums) {
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
        }

        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] m, int[] n) {
                return m[1] - n[1];
            }
        });
        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {
                    queue.poll();
                    queue.offer(new int[]{num, count});
                }
            } else {
                queue.offer(new int[]{num, count});
            }
        }
        int[] ret = new int[k];
        for (int i = 0; i < k; ++i) {
            ret[i] = queue.poll()[0];
        }
        return ret;
    }
}

题解3(快速排序)

java 复制代码
import java.util.*;

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 【1. 统计频率】:老规矩,用哈希表记录每个数字的出现次数
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums) {
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
        }
        
        // 【2. 转换结构】:将 Map 转换为 List,里面存的是 [数字, 频率] 数组
        // 这里用 List 是因为后面的快速选择算法需要通过下标频繁交换元素
        List<int[]> values = new ArrayList<int[]>();
        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            values.add(new int[]{num, count});
        }
        
        // 准备结果数组
        int[] ret = new int[k];
        
        // 【3. 执行快速选择】:传入 List、起始下标、结束下标、结果数组、结果数组的写入位、要找的 k
        qsort(values, 0, values.size() - 1, ret, 0, k);
        return ret;
    }

    /**
     * 快速选择核心算法
     * @param retIndex 当前准备把结果写到 ret 数组的哪个位置
     * @param k 当前还需要在当前区间找多少个最高频的元素
     */
    public void qsort(List<int[]> values, int start, int end, int[] ret, int retIndex, int k) {
        // 【核心优化:随机选择基准值】
        // 随机挑一个位置,把它和 start (第一个元素) 交换。
        // 这是为了防止数组如果是极端倒序的情况,导致算法退化成 O(N^2)
        int picked = (int) (Math.random() * (end - start + 1)) + start;
        Collections.swap(values, picked, start);
        
        // pivot 记录基准值的"频率"
        int pivot = values.get(start)[1];
        
        // index 用来充当分界线指针。它左边的最终都会是 >= pivot 的,右边的都会是 < pivot 的
        int index = start;
        
        // 【开始分区 (Partition)】
        for (int i = start + 1; i <= end; i++) {
            // 如果当前元素的频率 >= 基准值的频率
            if (values.get(i)[1] >= pivot) {
                // 就把它交换到左边来,同时分界线指针向右挪一位
                Collections.swap(values, index + 1, i);
                index++;
            }
        }
        // 循环结束后,把最开始放在 start 的基准值,换回到真正的分界线位置 index 上
        Collections.swap(values, start, index);

        // 【开始剪枝 (Pruning)】------ 快速选择与快速排序最大的区别
        // index - start 代表:在基准值左边(频率严格大于或等于基准值的),一共有多少个元素
        if (k <= index - start) {
            // 情况 A:左边的元素数量已经大于等于我们需要的 k 个了!
            // 说明我们要找的高频元素全都在左半区,右半区直接扔掉不管。
            qsort(values, start, index - 1, ret, retIndex, k);
        } else {
            // 情况 B:左边的元素数量不够 k 个,或者刚好连同基准值一起凑够。
            // 那么,左边这些元素(从 start 到 index)一定全都是前 k 名的成员!把它们全部收下。
            for (int i = start; i <= index; i++) {
                ret[retIndex++] = values.get(i)[0];
            }
            
            // 把左边和基准值收下后,看看还差几个才够 k 个?
            // 如果还差(k > 左边收下的个数),就只去右半区继续找剩下的名额
            if (k > index - start + 1) {
                // 还要找的名额 = 总 k - 已经收下的个数 (index - start + 1)
                qsort(values, index + 1, end, ret, retIndex, k - (index - start + 1));
            }
        }
    }
}

思路

这道题与上一道题解决方法类似,同样都可以使用快速选择与堆来进行实现,相比之下堆确实简单些,快速选择这一道题更难一些,特别是那个交换交换操作哨位比较难以理解,相比与上道题的换操作,我还是更喜欢上一个官解操作的交换过程,也就可以写为

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 1. 统计频率
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        // 2. 转换成二维数组 values[i][0] 是数字,values[i][1] 是频率
        int n = map.size();
        int[][] values = new int[n][2];
        int idx = 0;
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            values[idx][0] = entry.getKey();
            values[idx][1] = entry.getValue();
            idx++;
        }

        // 3. 使用你喜欢的 Hoare 分区快速选择算法
        quickselect(values, 0, n - 1, k);

        // 4. 提取前 k 个结果
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = values[i][0];
        }
        return res;
    }

    // 你熟悉的 Hoare 快速选择核心代码
    private void quickselect(int[][] values, int l, int r, int k) {
        if (l >= r) return;

        // 选取基准值的频率(取最左侧)
        int pivotFreq = values[l][1];
        int i = l - 1, j = r + 1;

        // 开始相向双指针交替扫描
        while (i < j) {
            // 注意:因为我们要找"最高频",所以是降序排列!
            // i 找比基准值小的(不该在左边的)
            do i++; while (values[i][1] > pivotFreq);
            // j 找比基准值大的(不该在右边的)
            do j--; while (values[j][1] < pivotFreq);

            if (i < j) {
                // 交换一维数组的引用
                int[] temp = values[i];
                values[i] = values[j];
                values[j] = temp;
            }
        }

        // 分区结束后,[l, j] 是高频区,[j+1, r] 是低频区。
        // 我们要找前 k 名(对应的下标是 0 到 k-1)。
        // 如果边界 j 已经覆盖了 k-1 这个下标,说明前 k 名都在左半边,继续切分左半边即可。
        if (j >= k - 1) {
            quickselect(values, l, j, k);
        } else {
            // 否则说明左半边的高频元素不够 k 个,还需要去右半边找剩下的
            quickselect(values, j + 1, r, k);
        }
    }
}

当然你觉得原来的交换好理解也可以用原来的方法,我是偏向于上道题的交换。

相关推荐
极客先躯1 小时前
高级java每日一道面试题-2025年7月17日-基础篇[LangChain4j]-如何实现模型的负载均衡和故障转移?
java·langchain·负载均衡·重试机制·负载均衡实现·故障转移实现·多级降级
何中应1 小时前
使用jvisualvm提示“内存不足”
java·jvm·后端
何中应1 小时前
如何手动生成一个JVM内存溢出文件
java·jvm·后端
小灵吖1 小时前
LangChain4j Tool(Function Call)
java·后端
Lxinccode1 小时前
AI编程(3) / claude code[3] : 更新apiKey
java·数据库·ai编程·claude code
小灵吖2 小时前
LangChain4j Prompt 提示词工程
java·后端
载数而行5202 小时前
算法系列2之最短路径
c语言·数据结构·c++·算法·贪心算法
资深web全栈开发2 小时前
CoI - 组合优于继承:解耦的艺术
android·java·开发语言