题目

给你一个整数数组 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);
}
}
}
当然你觉得原来的交换好理解也可以用原来的方法,我是偏向于上道题的交换。