# 「力扣」第 347 题:前 K 个高频元素(中等)

# 题目描述

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

示例 1:

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

示例 2:

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

提示:

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

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

# 题意分析

  • 题目中说「出现频率前 k 高的元素」,因此我们需要计算频率,按照频率从高到低找出这些元素;

  • 题目中还说「可以按 任意顺序 返回答案」,这是典型的 TopK 问题解决的问题,因此有两种方法:

    • 方法一:使用优先队列找出 频率 最大的前 K 个元素;
    • 方法二:使用快速排序的子过程找出前 K 个元素。
  • 再看到数据的范围:

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

    说明 k 的值不会给负数,也不会超过数组中不相同的元素的个数,一些特殊的情况不用考虑。

# 方法一:优先队列(堆)

思路:找前 k 大,所以用小顶堆,堆里的所有元素都小于等于堆顶元素,比堆顶还小的丢弃。

参考代码 1

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class Solution {

    public int[] topKFrequent(int[] nums, int k) {
        int len = nums.length;

        Map<Integer, Integer> freq = new HashMap<>();
        for (int num : nums) {
            freq.put(num, freq.getOrDefault(num, 0) + 1);
        }

        // 按照频数升序排序
        PriorityQueue<int[]> minHeap = new PriorityQueue<>(len, Comparator.comparingInt(o -> o[1]));
        for (Map.Entry<Integer, Integer> entry : freq.entrySet()) {
            Integer num = entry.getKey();
            Integer count = entry.getValue();

            if (minHeap.size() < k) {
                minHeap.add(new int[]{num, count});
            } else {
                if (count > minHeap.peek()[1]) {
                    // 把堆顶元素弹出,然后放入新的元素
                    minHeap.poll();
                    minHeap.add(new int[]{num, count});
                }
            }
        }

        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = minHeap.poll()[0];
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度:,这里 是数组的长度;
  • 空间复杂度:,记录频数的哈希表的长度为 ,优先队列(堆)的长度为

# 方法二:快速排序的子过程

思路:

  • 步骤 1:统计每一个元素出现的次数;
  • 步骤 2:把元素的值和频数绑定,放在一个数组中;
  • 步骤 3:在 values 上执行 partition 找到下标 k - 1 的位置,当 leftright 重合的时候,前 k 个元素就是题目要求的结果

参考代码 2

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Solution {

    public int[] topKFrequent(int[] nums, int k) {
        // 步骤 1:统计每一个元素出现的次数
        Map<Integer, Integer> freq = new HashMap<>();
        for (int num : nums) {
            freq.put(num, freq.getOrDefault(num, 0) + 1);
        }

        // 步骤 2:把元素的值和频数绑定,放在一个数组中
        // 因为不知道有多少个不同元素,所以用动态数组
        List<int[]> values = new ArrayList<>();
        for (Map.Entry<Integer, Integer> entry : freq.entrySet()) {
            int num = entry.getKey();
            int count = entry.getValue();
            values.add(new int[]{num, count});
        }

        // 步骤 3:在 values 上执行 partition 找到下标 k - 1 的位置,当 left 与 right 重合的时候,前 k 个元素就是题目要求的结果
        int left = 0;
        int right = values.size() - 1;
        int target = k - 1;
        while (left <= right) {
            int pIndex = partition(values, left, right);
            if (pIndex < target) {
                left = pIndex + 1;
            } else if (pIndex > target) {
                right = pIndex - 1;
            } else {
                break;
            }
        }

        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = values.get(i)[0];
        }
        return res;
    }

    public int partition(List<int[]> values, int left, int right) {
        int randomIndex = (int) (Math.random() * (right - left + 1)) + left;
        Collections.swap(values, randomIndex, left);

        int pivot = values.get(left)[1];
        int j = left;
      	// 循环不变量定义:
        // [left + 1.. j] >= pivot
        // (j, i) < pivot
        // 出现频率前 k 高的元素,降序排序,下标是 k - 1
        for (int i = left + 1; i <= right; i++) {
            if (values.get(i)[1] >= pivot) {
                j++;
                Collections.swap(values, j, i);
            }
        }
        Collections.swap(values, left, j);
        return j;
    }
}

复杂度分析

  • 时间复杂度:,这里 是数组的长度。最坏情况下,时间复杂度为 ,由于随机选择切分元素,这种最坏的情况出现的概率很低很低;
  • 空间复杂度:,记录频数的哈希表的长度为 ,优先队列(堆)的长度为

作者:liweiwei1419 链接:https://suanfa8.com/priority-queue/solutions/0347-top-k-frequent-elements 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Last Updated: 11/19/2024, 1:33:17 AM