📝 题目描述

题目链接前 K 个高频元素

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
示例 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 <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

💡 解题思路

方法一:桶排序

在长度为 N 的数组中,任何一个元素出现的频率(次数),最少是 1 次,最多也就是 N 次。

既然频率的范围被严格限制在了 [1,N][1,N] 这个有限的区间内,我们就不需要通过“比较”来排序(比如堆排序或快排),而是可以直接把频率作为数组的下标(索引)。

步骤

  1. 统计频率:使用 unordered_map 统计每个数字出现的次数。
  2. 构建桶:创建一个长度为 N+1N+1 的数组(为了让下标能取到 NN)。这个数组的每一个位置就是一个桶。
    • 下标 i:代表元素出现的频率。
    • 桶内的内容:存储所有出现频率恰好为 i 的元素。由于可能有多个不同的数字出现频率相同,所以每个桶里需要放一个数组。
  3. 数字入桶:遍历第一步得到的哈希表,把数字根据它的频率扔进对应的桶里。
  4. 倒序取数字:因为我们要找的是“频率前 k 高”的元素,而桶的下标越大代表频率越高。所以我们只需从后往前遍历这个桶数组,依次把桶里的数字拿出来放入结果集,直到拿够 k 个为止。

方法二:小顶堆

既然我们只需要前 k 个最高频的元素,我们不需要把所有的 M 个元素都维护在堆里。我们可以维护一个大小固定为 k 的小顶堆。

思路

  1. 先用哈希表统计频率。
  2. 遍历哈希表,将元素逐个放入小顶堆中。
  3. 如果堆的大小超过了 k,我们就把堆顶元素(也就是当前堆里频率最低的元素)弹出。
  4. 遍历结束后,留在堆里的 k 个元素,就是我们要找的出现频率前 k 高的元素。

🔧 代码实现

1、桶排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
// 1. 统计每个元素的频率
unordered_map<int, int> mp;
for (const auto& num : nums) {
mp[num]++;
}

// 2. 创建桶。
// 数组大小为 nums.size() + 1,保证频率最高为 nums.size() 时不会越界
// buckets[i] 存储的是所有出现频率为 i 的数字
int n = nums.size();
vector<vector<int>> buckets(n + 1);

// 3. 将数字按照频率放入对应的桶中
for (const auto& p : mp) {
int num = p.first;
int freq = p.second;
buckets[freq].push_back(num);
}

// 4. 倒序遍历桶,收集前 k 个高频元素
vector<int> res;
for (int i = n; i >= 0 && res.size() < k; --i) {
// 如果当前频率的桶里有元素,就把它们加入结果集
if (!buckets[i].empty()) {
for (int num : buckets[i]) {
res.push_back(num);
// 如果结果集已经收集够了 k 个元素,直接返回
if (res.size() == k) {
return res;
}
}
}
}

return res;
}
};

2、小顶堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
struct compare {
bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
};

vector<int> topKFrequent(vector<int>& nums, int k) {
priority_queue<pair<int, int>, vector<pair<int, int>>, compare> pq;
unordered_map<int, int> mp;
vector<int> res;
for(const auto& num : nums) {
mp[num]++;
}
for(const auto& p : mp) {
if (pq.size() < k) {
pq.push({p.first, p.second});
} else if (p.second > pq.top().second) {
pq.pop();
pq.push({p.first, p.second});
}
}
for(int i = 0; i < k; i++) {
res.push_back(pq.top().first);
pq.pop();
}
return res;
}
};

📊 复杂度分析

1、桶排序

  • 时间复杂度O(N)O(N),遍历数组建哈希表 O(N)O(N);遍历哈希表入桶,哈希表中最多有 NN 个元素,所以是 O(N)O(N);倒序遍历桶数组,最多遍历 N+1N+1 次桶数组,加上内部取出 kk 个元素,总体仍然是 O(N)O(N)
  • 空间复杂度O(N)O(N),哈希表最坏情况占用 O(N)O(N)

2、小顶堆

  • 时间复杂度O(N+Mlogk)O(N+Mlogk),遍历数组统计频率需要 O(N)O(N),同时因为堆的大小一直维持在 kk,每次 pushpushpoppop 的复杂度降到了 O(logk)O(logk)
  • 空间复杂度O(M)O(M),哈希表占用 O(M)O(M),堆占用 O(k)O(k),总空间为 O(M)O(M)

🎯 总结

  • 核心思想:了解桶排序的思路。