HOT100系列-堆类型题
核心思想
例题
1、数组中的第K个最大元素
题目描述:
给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
提示:
1 <= k <= nums.length <= 105-104 <= nums[i] <= 104
解题思路:
- 快排
这里简单介绍下快速排序算法:
普通的快速排序算法首先是要在待排序数组中随机选取一个数记作 num,比 num 小的都放于 num 左边,比 num 大的都放于右边,不断迭代这个过程,直到迭代的数组中左右边界相同为止,具体步骤如下
- 随机在数组左右边界 l, r 中选择一个数 num
- 对 (l, r) 内的数组元素进行遍历,这个过程要使比 num 小的元素都在左边,比 num 大的元素都在右边,最后将第一个遇见的 num 置于左与右的中间位置(其实就是 num 在 l, r 的排序后的对应的最后位置,这里假设有多个 num),返回 num 位置的索引 m
- 在(l, m)和(m+1, r)中执行上述步骤直到待排序数组片段的左边界=右边界(即待排序数组片段只有一个元素)
上述普通快排有一个非常大的问题就是当一个数组片段中有多个 num 时,它只排了其中一个,其余都未排到对应位置
而荷兰国旗的快速排序就对其做了优化,使得将所有 num 都排到了数组片段的对应位置,提高了算法效率
荷兰国旗的优化相对于普通快排最大的地方在于一次将将所有 num 都排到了数组片段的对应位置,在这个排序的过程中如下
已知arr[l... r]范围上一定有 num这个值
- 划分数组 <x放左边,x放中间,>x放右边
- 把全局变量first, last,更新成x区域的左右边界
- 小于num的放first左边,大于num的放last右边
- 若arr[i] < num 则swap (first, i), first++, i++
- 若arr[i]>num 则swap (last, i), last--
- 若arr[i] num 则i++
代码如下:
Java
class Solution {
public static int first;
public static int last;
public int findKthLargest(int[] nums, int k) {
return randomizedSelect(nums,nums.length-k);
}
//从num数组中挑选出第k大的元素
public static int randomizedSelect(int[]nums,int k){
int ans=0;
for(int l=0,r=nums.length-1,num;l<=r;){
//随机选择数字
num=nums[l+(int)(Math.random()*(r-l+1))];
//荷兰国旗的快排
partition(nums,l,r,num);
//看当前随机数字排序位置
//有一些二分的思想
if(first>k){
//说明第K大的数在num左侧
//接下来只管排左侧
r=first-1;
}else if(last<k){
//说明第K大的数在num右侧
//接下来只管排右侧
l=last+1;
}else{
//第K大的数值在first和last之间
//则由快排确定第K大元素
ans=nums[k];
return ans;
}
}
return ans;
}
//荷兰国旗快排
//将num数值排在数组正确位置,从小到大
public static void partition(int[]nums,int l,int r,int num){
first=l;
last=r;
int i=l;
while(i<=last){
if(nums[i]<num){
//first左侧全部放比num小的元素
swap(nums,i,first);
first++;
i++;
}else if(nums[i]>num){
swap(nums,i,last);
last--;
}else{
i++;
}
}
}
public static void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
2、前 K 个高频元素
题目描述:
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
**输入:**nums = [1,1,1,2,2,3], k = 2
输出:[1,2]
示例 2:
**输入:**nums = [1], k = 1
输出:[1]
示例 3:
**输入:**nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出:[1,2]
提示:
1 <= nums.length <= 105k的取值范围是[1, 数组中不相同的元素的个数]- 题目数据保证答案唯一,换句话说,数组中前
k个高频元素的集合是唯一的
**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
解题思路:
- 收集元素频次
- 将<元素,元素频次> 放进大根堆中,以元素频次排序
- 取大顶堆前k个即可
代码如下
java
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> map=new HashMap<>();
int n=nums.length;
//收集元素的频次
for(int i=0;i<n;i++){
map.put(nums[i],map.getOrDefault(nums[i],0)+1);
}
//创建大根堆
PriorityQueue<int[]>heap=new PriorityQueue<>((a,b)->b[1]-a[1]);
for(int key:map.keySet()){
heap.offer(new int[]{key,map.get(key)});
}
int[]ans=new int[k];
for(int i=0;i<k;i++){
ans[i]=heap.poll()[0];
}
return ans;
}
}
3、数据流的中位数
题目描述:
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
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以内的答案将被接受。
示例 1:
输入
"MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"
\[\], \[1\], \[2\], \[\], \[3\], \[\]
输出
null, null, null, 1.5, null, 2.0
解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
提示:
-105 <= num <= 105- 在调用
findMedian之前,数据结构中至少有一个元素 - 最多
5 * 104次调用addNum和findMedian
解题思路
- 1、将较大的元素放入小顶堆中,将较小的元素放入大顶堆中,这样从大顶堆底部到顶部,从小顶堆的顶部到底部就相当于对加入的数据进行了排序
- 2、在排序的基础上,维持大小顶堆的size之差必须小于2,那么就相当于将中位数永远保持在了大小顶堆的顶部,比较大小顶堆size大小关系,即可找出中位数(具体看findMedian方法)
代码如下
Java
class MedianFinder {
//大根堆,存放较小的数字
private PriorityQueue<Integer> maxheap;
//小根堆,存放较大的数字
private PriorityQueue<Integer> minheap;
public MedianFinder() {
maxheap=new PriorityQueue<>((a,b)->b-a);
minheap=new PriorityQueue<>((a,b)->a-b);
}
public void addNum(int num) {
if(maxheap.isEmpty() || maxheap.peek()>=num){
maxheap.add(num);
}else{
minheap.add(num);
}
//要注意保持大小根堆的平衡,即size差不能超过2
banlance();
}
public void banlance(){
if(Math.abs(maxheap.size()-minheap.size())==2){
if(maxheap.size()>minheap.size()){
minheap.add(maxheap.poll());
}else{
maxheap.add(minheap.poll());
}
}
}
public double findMedian() {
if(maxheap.size()==minheap.size()){
return (double)(maxheap.peek()+minheap.peek())/2.0;
}else{
return maxheap.size()>minheap.size()?maxheap.peek():minheap.peek();
}
}
}