在数据处理和算法面试中,"前K个高频元素"是一个经典问题。给定一个整数数组nums和一个整数k,我们需要返回数组中出现频率前k高的元素。这个问题看似简单,但要在O(n log n)时间复杂度内解决并不容易,更不用说题目要求的优于O(n log n)的进阶解法了。
首先我们需要明确几个关键点:
传统解决Top K问题的方法通常使用堆(优先队列),时间复杂度为O(n log k)。但我们可以做得更好——利用桶排序的思想,将时间复杂度优化到O(n)。
核心思路是将"频率"作为数组下标:
这种方法的巧妙之处在于:
首先我们需要统计每个数字出现的频率。这里使用HashMap是最自然的选择:
java复制Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
提示:Java 8的merge方法可以更简洁地实现频率统计:
java复制frequencyMap.merge(num, 1, Integer::sum);
接下来我们创建桶数组,其中索引代表频率,值是该频率下的所有数字列表:
java复制List<Integer>[] buckets = new List[nums.length + 1];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<>();
}
for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
buckets[entry.getValue()].add(entry.getKey());
}
这里有几个关键细节:
最后我们从最高频率开始,倒序遍历桶数组,收集前k个高频元素:
java复制int[] result = new int[k];
int index = 0;
for (int i = buckets.length - 1; i >= 0 && index < k; i--) {
for (int num : buckets[i]) {
result[index++] = num;
if (index == k) break;
}
}
return result;
我们可以通过记录最大频率来减少桶数组的大小:
java复制int maxFrequency = Collections.max(frequencyMap.values());
List<Integer>[] buckets = new List[maxFrequency + 1];
这样桶数组的大小从O(n)降为O(maxFrequency),在多数实际场景中可以节省空间。
当多个数字具有相同频率时,题目允许返回任意顺序。如果需要特定顺序(如数值大小),可以在放入结果前对桶内元素排序:
java复制for (List<Integer> bucket : buckets) {
if (bucket != null) {
Collections.sort(bucket);
}
}
对于大规模数据,我们可以将频率统计和桶填充阶段并行化:
java复制frequencyMap = Arrays.stream(nums)
.parallel()
.boxed()
.collect(Collectors.toConcurrentMap(
num -> num,
num -> 1,
Integer::sum
));
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 排序法 | O(n log n) | O(n) | 简单但不符合进阶要求 |
| 最小堆 | O(n log k) | O(n) | 通用解法,k较小时高效 |
| 桶排序 | O(n) | O(n) | 频率范围有限时最优 |
这种算法在实际中有广泛应用:
当数据量极大时,可以考虑:
掌握这个算法后,可以解决一系列类似问题:
常见错误是忘记初始化桶:
java复制List<Integer>[] buckets = new List[size]; // 仅创建数组,元素为null
// 必须初始化每个元素
for (int i = 0; i < size; i++) {
buckets[i] = new ArrayList<>();
}
需要特别注意的边界情况:
当性能不理想时,可以:
以下是完整的Java实现,包含所有优化和边界处理:
java复制public int[] topKFrequent(int[] nums, int k) {
// 边界条件检查
if (nums == null || nums.length == 0 || k <= 0) {
return new int[0];
}
// 1. 频率统计
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.merge(num, 1, Integer::sum);
}
// 2. 构建桶数组
int maxFreq = Collections.max(freqMap.values());
List<Integer>[] buckets = new List[maxFreq + 1];
Arrays.setAll(buckets, i -> new ArrayList<>());
freqMap.forEach((num, freq) -> buckets[freq].add(num));
// 3. 收集结果
int[] result = new int[Math.min(k, freqMap.size())];
int index = 0;
for (int i = maxFreq; i >= 0 && index < result.length; i--) {
for (int num : buckets[i]) {
result[index++] = num;
if (index == result.length) {
return result;
}
}
}
return result;
}
全面的测试应该包括:
java复制nums = [1,1,1,2,2,3], k = 2 → [1,2]
java复制nums = [1,1,1], k = 1 → [1]
java复制nums = [1,2,2,3,3,3], k = 3 → [3,2,1]
java复制nums = [1], k = 1 → [1]
nums = [], k = 0 → []
现代Java提供了更简洁的实现方式:
java复制public int[] topKFrequent(int[] nums, int k) {
return Arrays.stream(nums)
.boxed()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.sorted(Map.Entry.<Integer, Long>comparingByValue().reversed())
.limit(k)
.mapToInt(Map.Entry::getKey)
.toArray();
}
虽然这段代码更简洁,但时间复杂度是O(n log n),不符合进阶要求。
对于性能敏感场景,可以考虑使用原始类型集合:
java复制Int2IntOpenHashMap freqMap = new Int2IntOpenHashMap(); // 来自FastUtil
为了更好地理解算法流程,我们可以用以下例子演示:
输入:nums = [4,1,-1,2,-1,2,3], k = 2
频率统计:
{
4: 1,
1: 1,
-1: 2,
2: 2,
3: 1
}
构建桶数组:
buckets[0] = []
buckets[1] = [4, 1, 3]
buckets[2] = [-1, 2]
收集结果:
最终结果:[-1, 2](顺序不重要)