LeetCode刷题记录----347.前K个高频元素(Medium)
2025/9/17
题目(Medium):
347. 前 K 个高频元素
我的思路:
模式识别:在一组数据中,找到前几大的元素 --->可以考虑用堆来处理
思路1:最大堆
因为是要返回前K大的元素,我们可以构建一个最大堆,然后把所有元素都入堆之后。再把堆中的 K 个出堆,即可得到需要的K个元素
不过这里要的是出现频率的前K大,所以需要先构建出现数字和出现频率的字典映射。而最大堆,可以用优先队列,里面优先级用当前加入堆中元素的负数表示即可。
所以大致步骤是:
①遍历原始数组,并用字典记录每个数字的出现频率
②遍历字典,用优先队列构建最大堆
③把最大堆出堆K次,并把出堆元素加入到数组中。出完后即为答案
具体代码如下:
public class Solution {public int[] TopKFrequent(int[] nums, int k) {Dictionary<int, int> freDic = new Dictionary<int, int>();PriorityQueue<int, int> maxHeap = new PriorityQueue<int, int>();foreach(var num in nums){if(!freDic.ContainsKey(num))freDic.Add(num, 1);elsefreDic[num]++;}foreach(var key in freDic.Keys){maxHeap.Enqueue(key, -freDic[key]);}int[] res = new int[k];for(int i = 0; i < k; i++){res[i] = maxHeap.Dequeue();}return res;}
}
时间复杂度:O(NlogN)【这里影响最大的是遍历字典并入堆的部分,最坏的情况下字典的长度可以达到N,即原始数组中每个数字都不同。而堆中也会相应需要插入这么多数字】
空间复杂度:O(N)【字典或者堆中最多会储存N个元素】
思路2:最小堆
或者我们也可以用最小堆,只是一直保持最小堆中只有K个元素。当超过K个元素的时候,就会出堆,这样每次就可以把最小的元素排除。遍历完全之后剩下的堆中元素就是前K大元素了。
具体代码如下:
public class Solution {public int[] TopKFrequent(int[] nums, int k) {Dictionary<int, int> freDic = new Dictionary<int, int>();PriorityQueue<int, int> minHeap = new PriorityQueue<int, int>();foreach(var num in nums){freDic[num] = freDic.GetValueOrDefault(num, 0) + 1;}//让最小堆中只保留k个最大的元素foreach(var key in freDic.Keys){minHeap.Enqueue(key, freDic[key]);if(minHeap.Count > k)minHeap.Dequeue();}int[] res = new int[k];for(int i = 0; i < k; i++)res[i] = minHeap.Dequeue();return res;}
}
时间复杂度:O(NlogK)【遍历数组长度为N ,堆中最多有K个元素】
空间复杂度:O(N)【哈希表最多有N个元素】
这里是利用了最小堆每次出堆就是排除当前最小元素的特性。
优化思路:
模式识别:一组确定的数中,找到前K大的数 --->可以用快速排序中的基准值排序部分逻辑处理
思路:因为快速排序中,第一步是选定一个基准值,并把比它大和小的元素分别放置在两侧。而我们正好需要获取的是前K大的元素,而这前K大具体是怎么排列的我们并不关心。
因此,举例说明,选定好基准值4并排序好后数组为[5,6,4,2,3],基准值索引值为【2】,。我们把比4大的排在4的左边,如果k = 1,则我们可以发现前1大元素一定是在左半区间,也就是【0,2】这个范围,那我们只要再递归地缩小区间去【0,1】上继续检索即可。
那如果k = 4呢,此时左半区间【0,2】肯定都是前4大元素的,那就还剩下4-3 = 1个元素,在右半区间,而右半区间还没排序,因此我们也递归地缩小区间去【3,4】上检索即可。
注意当我们比较的时候,是通过频率比较,而具体加入答案数组的时候则是加入原始的值。这两个数据我们都需要,因此用元组形式(int, int)来对这两个信息同时进行存储。
还有,对于出现频率的统计,我们依然采样字典的形式。
具体代码如下:
public class Solution {private List<(int, int)> pairs = new List<(int, int)>(); //存储(原始数字,出现频率)private List<int> res = new List<int>(); //答案列表public int[] TopKFrequent(int[] nums, int k) {Dictionary<int, int> freDic = new Dictionary<int, int>();foreach(var num in nums){freDic[num] = freDic.GetValueOrDefault(num, 0) + 1;}foreach(var key in freDic.Keys){pairs.Add((key, freDic[key]));}Qsort(0, pairs.Count-1, k);return res.ToArray(); //把列表转为数组返回}public void Swap(int i, int j){if(i == j) return;var temp = pairs[i];pairs[i] = pairs[j];pairs[j] = temp;}public void Qsort(int left, int right, int k){int randomIndex = new Random().Next(left, right+1);//int randomIndex = left;int priovt = pairs[randomIndex].Item2;Swap(randomIndex, left);int store_Index = left;for(int i = left+1; i <= right; i++){if(pairs[i].Item2 > priovt){Swap(store_Index + 1, i);store_Index++;}}Swap(store_Index, left); //最后把基准值换到正确的位置Console.WriteLine("store_index = " + store_Index + " , priovt = " + priovt);//接下来根据基准值所在的索引值和k的大小关系来获取前k大的数字if(k <= store_Index - left){//说明在左半区间,需要进一步减小范围Qsort(left, store_Index-1, k);}else{//说明是左半区的全部 + 右半区的一些for(int i = left; i <= store_Index; i++)res.Add(pairs[i].Item1);//看看右半区还剩多少if(k - (store_Index - left + 1) > 0){//对剩下的右半区再进行排序缩小区间Qsort(store_Index + 1, right, k-(store_Index-left+1));}}}
}
时间复杂度:O(N^2)【每次选择基准值为左右两端的话就会这样,不过如果是选择随机位置的话,平均时间复杂度可以达到O(N)】
空间复杂度:O(N)【最坏情况字典空间为N,排序数组的空间为N,快速排序递归深度为logN】
总结:
①对于在一组数据中,获取其中第K大/小,或者前K大/小的问题,可以通过堆或者快速选择来处理
②对于前K大,这里可以利用最小堆每次弹出最小值的原理,在第一次遍历数组的时候,可以直接入堆,然后把最小值弹出,这样里面保留的就一直是前K大的元素
③对于快速选择,注意的是对基准值位置分割的左右区间的含义,以及对K的大小比较的关系的了解。