文章目录
一、最后一块石头的重量
这一题说白了就是碰撞石头,我们想想,我们如何利用一种算法求解
我们是不是每一次都要两块最重的石头啊,那是不是我们就可以知道是堆 ?
你看,如果我们建立大根堆,每次堆顶元素都是最重的石头
写堆排序代码还是非常考验能力的
为什么我们要选择大根堆,因为这一题都是要拿最重的石头,这样我们每次都可以快速选择两块最重的石头
java
class Solution {
public int lastStoneWeight(int[] stones) {
//使用大根堆,这样保证我们每次队头的两个元素永远是最大值
PriorityQueue<Integer> heap = new PriorityQueue<>((a,b)->b-a);
for(int stone : stones){
heap.offer(stone);
}
//比较消除
while(heap.size() > 1){
//这样stone1 >= stone2
int stone1 = heap.poll();
int stone2 = heap.poll();
if(stone1 > stone2){
heap.offer(stone1-stone2);
}
}
return heap.isEmpty() ? 0 : heap.poll();
}
}
二、数据流中第K大元素
这就是典型的TopK问题
如果题目中问我们第K大元素,我们就建立元素个数为K的大根堆
如果题目中问我们第K小元素,我们就建立元素个数为K的小根堆
还有,题目中问的是第K个排序后的元素,如果最终序列是这样2 2 3 3,要第二个最终序列则返回2,如果问的是第二个不重复的最终序列则返回3
当前算法是堆排序,其实还有一个快速选择排序,它时间复杂度可达到O(n)
java
class KthLargest {
//默认是小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
int size;
public KthLargest(int k, int[] nums) {
size = k;
for(int num : nums){
heap.offer(num);
//如果堆的大小超过了k,弹出堆顶元素
if(heap.size() > k){
heap.poll();
}
}
}
public int add(int val) {
heap.offer(val);
//判断
if(heap.size() > size){
heap.poll();
}
//注意不能弹出,因为后续插入其他数还要用到
return heap.peek();
}
}
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.add(val);
*/
三、前K个高频单词
这一题就有点难搞了,首先要保证单词频率是从大到小的,并且还要保证如果单词频次相同按照字段序从小到大
我们可以用一个数对Pair<String,Integer>去存,对于单词建立大根堆,对于频次建立小根堆
具体就是,我们先去统计每个单词出现次数
再去创建一个大小为K的频次小根堆,单词大根堆
循环维护进堆出堆并判断
最终我们的结果是一个逆序的,要记得反转,当然你可以逆着填表最后返回也行
java
class Solution {
public List<String> topKFrequent(String[] words, int k) {
//预处理字符串,统计出现次数
HashMap<String,Integer> hash = new HashMap<>();
for(String str : words){
hash.put(str,hash.getOrDefault(str,0)+1);
}
//对于频次创建小根堆,内部字典序是大根堆
PriorityQueue<Pair<String,Integer>> heap = new PriorityQueue<>((a,b)->{
if(a.getValue().equals(b.getValue())){
//说明两个频次相同,按照字典序排序,如果compareTo结果为负,则b排在前面
return b.getKey().compareTo(a.getKey());
}
//否则就按照正常频次排序
return a.getValue()-b.getValue();
});
//循环进队堆判断
for(Map.Entry<String,Integer> e : hash.entrySet()){
heap.offer(new Pair<>(e.getKey(),e.getValue()));
//判断
if(heap.size() > k){
heap.poll();
}
}
//统计结果
List<String> ret = new ArrayList<>();
while(!heap.isEmpty()){
ret.add(heap.poll().getKey());
}
//注意返回结果要逆序
Collections.reverse(ret);
return ret;
}
}
四、数据流的中位数------Hard
这一题非常不好想,我们看到这题第一眼就是每次插入一个数字后调用库函数sort()一下,但是这样会有O(nlogn)的时间复杂度
再者,我们可以进行插入排序,每次插入一个数都找到位置插入,但是这样要O(n)
那有没有更快的方法呢,有,就是要利用双堆排序思想,也就是大小堆维护中位数

每次我们插入一个数,首先要判断这个数要放去哪,但是在此之前,我们要判断两个堆状态
我们用x表示left大根堆的堆顶元素,用y表示right小根堆的堆顶元素
- 如果两个堆元素个数相同。若
num<=x,此时要进入左边大根堆,满足left元素个数-right元素个数 >=0且<=1;若num > x,此时要进入右边的小根堆,但是此时会导致两边都堆元素个数不一样,怎么办呢?我们可以在插入右边小根堆后,紧接着让right小根堆的堆顶元素放入left大根堆,此时就平衡了 - 如果两个堆的元素个数不同。若
num<=x,此时要进入左边大根堆,但是此时会导致左边大根堆的元素个数不满足要求,一样的,我们插入left大根堆后,让left大根堆的堆顶元素进入right小根堆,此时元素个数达到平衡;若num>x,正常进入right小根堆就好
java
class MedianFinder {
//大根堆
PriorityQueue<Integer> left = new PriorityQueue<>((a,b)->{
return b-a;
});
//小根堆
PriorityQueue<Integer> right = new PriorityQueue<>((a,b)->{
return a-b;
});
//我们要始终保持left的元素个数>=right的元素个数且right元素个数不超过left+1
public MedianFinder(){}
public void addNum(int num) {
//插入数字的时候进行判断
if(left.size() == right.size()){
//判断这个数字进行哪个堆,如果是空堆直接进
if(left.isEmpty() || num <= left.peek()){
left.offer(num);
}else{
//先让num进右边
//要让右边堆顶元素放入左边,维持两个堆数量平衡
right.offer(num);
left.offer(right.poll());
}
}else{
if(num <= left.peek()){
//进入左边
left.offer(num);
//此时左边堆顶元素要去右边
right.offer(left.poll());
}else{
//反之进入右边
right.offer(num);
}
}
}
public double findMedian() {
//如果两个堆规模相等,说明是偶数个列表
return left.size() == right.size() ? (left.peek()+right.peek())/2.0 : left.peek();
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
感谢你的阅读
END