文章目录
一、数据流中的第 K 大元素
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
解题思路
- 使用PriorityQueue解决TopK问题
代码实现及解析
java
class KthLargest {
public int _k;
public PriorityQueue<Integer> heap;
public KthLargest(int k, int[] nums) {
this._k=k;
this.heap=new PriorityQueue<>();//建小根堆(Integer实现了Comparable接口,不用再传比较器了)
//将元素放到堆中
for(int x:nums){
heap.offer(x);
if(heap.size()>_k){//如果heap大小超过了k,就将堆顶元素弹出,最后堆中剩下的就是前k大元素
heap.poll();
}
}
}
public int add(int val) {
heap.offer(val);
if(heap.size()>_k) heap.poll();//仍然要始终维持堆的大小为k
return heap.peek();
}
}
总结
本题使用PriorityQueue的方法与以往不同:如果heap大小超过了k,就立即将堆顶元素弹出,最后堆中剩下的就是前k大元素,这个方法的代码实现比较方便,以后可以使用这个
二、前K个高频单词
给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
解题思路
- 先使用哈希表将单词的出现频次记录,再建立合适的堆结构,将元素放入。
代码实现及解析
java
class Solution {
public List<String> topKFrequent(String[] words, int k) {
List<String> list=new ArrayList<>();
//1.使用哈希表将单词的出现频次记录
Map<String,Integer> hash=new HashMap<>();
for(String str:words){
hash.put(str,hash.getOrDefault(str,0)+1);
}
//2.建立合适的堆结构,将元素放入
PriorityQueue<Map.Entry<String,Integer>> heap=new PriorityQueue<>((a,b)->{
if(a.getValue().equals(b.getValue())){//注意用equals()
return b.getKey().compareTo(a.getKey());//字典序(升序),相当于说"前k小",所以建大根堆
}else{
return a.getValue()-b.getValue();//频次不同,按频次从大到小,所以建小根堆
}
});
for(Map.Entry<String,Integer> entry:hash.entrySet()){
heap.offer(entry);
if(heap.size()>k){
heap.poll();
}
}
//3.取出结果
while(!heap.isEmpty()){
list.add(heap.poll().getKey());
}
Collections.reverse(list);
return list;
}
}
总结
哈希表的应用+TopK问题可以注意一下第2点Lambda表达式中的方法主体实现,分别构建了大根堆和小根堆,已经注释中解释的为什么要分别构建了大根堆和小根堆
三、数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。
与实际答案相差 10-5 以内的答案将被接受。
解题思路
方法一:
- 直接调用Collections.sort(),然后去中位数。排序的时间复杂度为O(nlogn),每次执行add()操作都要进行这样的排序。
方法二:
- 换一个排序思路,每次add()之前,数据都是有序的,那么add()时就只需要找到num的正确的位置将其插入就行了。所以我们使用插入排序的思想,遍历数据找到插入位置,这样时间复杂度变为了O(n),但还是太大。
方法三:
- "大、小根堆法",用两个堆(PriorityQueue)来维护数据,一个是大根堆,一个是小根堆。将有序数据分为两半,左半部分用leftHeap(大根堆)储存,右半部分用rightHeap(小根堆)来储存,元素个数为奇数时我们令leftHeap的元素比rightHeap的元素个数多1(谁多谁少都一样,处理方式换一下位置而已)。两个堆的大小一样时,
leftHeap.peek()就是中位数,leftHeap大小比rightHeap大1时,(leftHeap.peek()+rightHeap.peek())/2.0就是中位数。 - 在add数据时,如果
num<leftHeap.peek(),此时num只能放到leftHeap中,不然就一定不符合排序顺序,同样如果num>leftHeap.peek(),此时num就只能放到rightHeap中,不然就一定不符合排序顺序(两个堆都为空时,放入leftHeap)。这样num的放入那个堆就是确定的,就无法保证leftHeap大小等于rightHeap大小或者rightHeap大小+1。我们在num入堆后分情况处理一下就行,将两个堆的大小平衡手动再调整过来。
代码实现及解析
java
class MedianFinder {
public PriorityQueue<Integer> leftHeap;
public PriorityQueue<Integer> rightHeap;
public MedianFinder() {
leftHeap=new PriorityQueue<>((a,b)->b-a);//大根堆
rightHeap=new PriorityQueue<>((a,b)->a-b);//小根堆
}
public void addNum(int num) {
if(leftHeap.size()==rightHeap.size()){//add之前,if两个堆的大小相等
if(leftHeap.isEmpty()||num<=leftHeap.peek()){//此时num只能放到leftHeap中,不然就一定不符合排序顺序
leftHeap.offer(num);
}else{//num>leftHeap.peek(),此时num就只能放到rightHeap中,不然就一定不符合排序顺序
rightHeap.offer(num);
leftHeap.offer(rightHeap.poll());//为了维持leftHeap大小始终等于rightHeap,或比rightHeap大1
}
}else{//add之前,if两个堆的大小不相等(也就是leftHeap==rightHeap+1)
if(num<=leftHeap.peek()){
leftHeap.offer(num);
rightHeap.offer(leftHeap.poll());//leftHeap的大小最多只能比rightHeap大1
//执行之后,leftHeap大小==rightHeap大小
}else{
rightHeap.offer(num);
//执行之后,leftHeap大小==rightHeap大小
}
}
}
public double findMedian() {
//取中位数
if(leftHeap.size()==rightHeap.size()){
return (leftHeap.peek()+rightHeap.peek())/2.0;
}else{
return leftHeap.peek();
}
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
总结
复习解题思路和代码实现及解析