题目解析
给定一个整数数组 nums 和一个整数 k,要求返回数组中频率前 k 高的元素。你可以假设输入数组的大小是 n,并且 k 是一个有效的整数(1 <= k <= n)。这个问题是一个典型的“频率统计”和“堆排序”结合的问题,涉及如何高效地统计元素出现的频率并选出频率最高的元素。
思路解析
1. 统计元素频率
首先,我们需要知道数组中每个元素的出现次数。这个可以通过一个哈希表(Map)来实现。哈希表可以快速地插入元素和更新计数,每个元素的出现次数存储在哈希表中,键为元素值,值为该元素的出现频率。
假设输入数组 nums 为 [1, 1, 1, 2, 2, 3],我们可以使用哈希表统计频率。遍历数组时,对于每个元素,我们将其作为键插入哈希表,并更新对应的频率。最终得到以下统计结果:
{
1: 3,
2: 2,
3: 1
}
在这个哈希表中,键是数组中的元素,值是这些元素的出现次数。
2. 使用最小堆来选出频率前 k 高的元素
一旦我们得到了每个元素的频率,接下来就需要找出频率最高的 k 个元素。为了做到这一点,可以使用 最小堆。
- 堆的特点:最小堆是一种完全二叉树结构,其中每个父节点的值都小于等于其子节点的值,因此堆顶元素是最小的。我们可以利用这一特点来始终保持堆中元素的大小为
k,并在堆中存储频率前k高的元素。 - 为什么使用最小堆:由于我们需要获取频率前
k高的元素,并且每次维护堆的大小为k,当堆中的元素数量超过k时,堆顶元素(即频率最小的元素)需要被移除,确保堆中始终保存频率最高的k个元素。
操作流程:
- 插入堆:遍历频率表,对于每个元素(即频率对),我们将其插入到最小堆中。
- 保持堆大小为
k:当堆的大小超过k时,移除堆顶元素(即频率最小的元素)。 - 返回结果:遍历堆中的所有元素,返回它们的键(即频率最高的元素)。
这样,堆最终会包含频率前 k 高的元素。
3. 整体算法的复杂度
- 统计频率:使用哈希表统计频率的时间复杂度是
O(n),其中n是数组nums的长度。我们需要遍历整个数组,将每个元素的频率存入哈希表。 - 堆操作:对于每个元素,我们需要将其插入到堆中,堆的大小保持为
k。每次插入操作的时间复杂度是O(log k),因此插入n个元素的总时间复杂度是O(n log k)。 - 提取结果:从堆中提取
k个元素的时间复杂度是O(k log k)。
所以,整体的时间复杂度是 O(n log k),空间复杂度是 O(n + k),其中 n 是数组的大小,k 是要求返回的频率前 k 个元素。
代码详解
import java.util.*;
public class Main {
public static List<Integer> solution(int[] nums, int k) {
// Step 1: 统计每个元素的频率
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// Step 2: 使用一个最小堆来找频率前 k 高的元素
// 使用 lambda 表达式来按照频率进行排序
PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(
(a, b) -> a.getValue() - b.getValue() // 按频率升序排序
);
// Step 3: 保持堆的大小为 k
for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
minHeap.offer(entry);
if (minHeap.size() > k) {
minHeap.poll(); // 移除频率最小的元素
}
}
// Step 4: 将堆中的元素添加到结果列表中
List<Integer> result = new ArrayList<>();
while (!minHeap.isEmpty()) {
result.add(minHeap.poll().getKey());
}
return result;
}
public static void main(String[] args) {
// 测试用例
int[] nums1 = {1, 1, 1, 2, 2, 3};
int[] nums2 = {1};
System.out.println(solution(nums1, 2)); // 输出:[1, 2]
System.out.println(solution(nums2, 1)); // 输出:[1]
}
}
代码解析
-
统计频率:
Map<Integer, Integer> frequencyMap = new HashMap<>(); for (int num : nums) { frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1); }这部分代码遍历了数组
nums,并使用哈希表frequencyMap存储每个数字出现的次数。getOrDefault(num, 0)方法用来获取元素的当前频率,如果该元素还没有出现在哈希表中,则返回默认值0,然后加1。 -
构造最小堆:
PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>( (a, b) -> a.getValue() - b.getValue() // 按频率升序排序 );PriorityQueue是 Java 提供的堆数据结构。这里使用了一个自定义的比较器,使得堆中的元素根据频率升序排列。堆中存储的是Map.Entry<Integer, Integer>,即每个元素及其频率。 -
维护堆的大小:
for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) { minHeap.offer(entry); if (minHeap.size() > k) { minHeap.poll(); // 移除频率最小的元素 } }遍历频率表,对于每个频率对,我们将其插入到堆中。如果堆的大小超过
k,就弹出堆顶元素(即频率最小的元素)。这样,堆最终将包含频率前k高的元素。 -
提取结果:
List<Integer> result = new ArrayList<>(); while (!minHeap.isEmpty()) { result.add(minHeap.poll().getKey()); } return result;将堆中的元素逐个取出,放入结果列表中。最终返回的列表即为频率前
k高的元素。
示例解释
输入示例1
输入:nums = [1, 1, 1, 2, 2, 3], k = 2
- 统计频率:频率表为
{1: 3, 2: 2, 3: 1}。 - 构建堆:将元素
1、2、3插入堆,堆中元素按频率升序排列。当堆的大小超过2时,移除频率最小的元素(即3)。 - 结果:堆中最终包含元素
1和2,返回[1, 2]。
输入示例2
输入:nums = [1], k = 1
- 统计频率:频率表为
{1: 1}。 - 构建堆:堆中只有一个元素
1,不需要移除任何元素。 - 结果:返回
[1]。
总结
通过哈希表统计频率,并使用最小堆选出频率前 k 高的元素,我们能够高效地解决这个问题。整体时间复杂度为 O(n log k),非常适合大规模数据处理。