剑指Offer(专项突破版)刷题笔记 | 第九章 堆

258 阅读3分钟

堆的基础知识

最大堆(最小堆):每个节点的值总是大于(小于)或等于其任意子节点的值 最大堆(最小堆)可以用数组进行保存,如果一个节点在数组中的下标是i,则其父节点的下标为(i-2)/2,它的左右子节点为2i+1,2i+2。

最大堆添加元素

新元素添加到数组的末尾,然后上浮

最大堆删除顶部元素

删除顶部元素后,将数组末尾元素交换到顶部,然后下沉

堆的应用

堆的最大特点:最大值或者最小值位于顶部,只需要O(n)O(n)时间就可以求出数据集合中的最大值或最小值

堆经常用来求取数据集合中最大或最小的k个元素

Q59:数据流的第k大数字

题目(简单):设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。   示例:
输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8

解题思路

利用最小堆解决,当最小堆元素小于k时,直接添加,若元素已经达到k,后来的元素和堆顶元素比较,如果比堆顶元素大,则弹出顶部元素,添加这一元素

class KthLargest {
    private PriorityQueue<Integer> minHeap;
    private int size;

    public KthLargest(int k, int[] nums) {
         size = k;
         minHeap = new PriorityQueue<>();
         for(int num:nums){
            add(num);
         }
    }
    
    public int add(int val) {
        if(minHeap.size()<size){
            minHeap.offer(val);
        }else if(val > minHeap.peek()){
            minHeap.poll();
            minHeap.offer(val);
        }
    return minHeap.peek();
    }
}

Q60:出现频率最高的k个数字

题目(中等):给定一个整数数组 nums 和一个整数 k ,请返回其中出现频率前 k 高的元素。可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]
public int[] topKFrequent(int[] nums, int k) {
    //键放数,值放次数
    Map<Integer,Integer> numToCount = new HashMap<>();
    for(int num:nums){
        numToCount.put(num,numToCount.getOrDefault(num,0) + 1);
    }

    Queue<Map.Entry<Integer,Integer>> minHeap = new PriorityQueue<>((e1,e2) -> e1.getValue() - e2.getValue());
    //最小堆中如果没有就直接加入,如果有就与堆顶元素比较,谁出现次数多留谁
    for(Map.Entry<Integer,Integer> entry :numToCount.entrySet()){
        if(minHeap.size() < k){
            minHeap.offer(entry);
        }else{
            if(minHeap.peek().getValue() < entry.getValue()){
                minHeap.poll();
                minHeap.offer(entry);
            }
        }
    }
    //存成一个数组
    int[] result = new int[k];
    int i = 0;
    for(Map.Entry<Integer,Integer> entry:minHeap){
        result[i] =  entry.getKey();
        i++;
    }

    return result;
}

Q61:和最小的k个数对

题目(中等):给定两个以升序排列的整数数组 nums1 和 nums2 , 以及一个整数 k 。定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。请找到和最小的 k 个数对 (u1,v1),  (u2,v2)  ...  (uk,vk) 。

 

示例 1:

输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

示例 2:

输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:[1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

示例 3:

输入: nums1 = [1,2], nums2 = [3], k = 3 
输出: [1,3],[2,3]
解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]

使用最大堆保存和最小的k个数对

public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
    Queue<int []> maxHeap = new PriorityQueue<>((p1,p2)->p2[0] + p2[1] - p1[0] - p1[1]);

    for(int i = 0; i < Math.min(k,nums1.length);i++){
        for(int j = 0;j < Math.min(k,nums2.length);j++){
            if(maxHeap.size() < k){
                maxHeap.offer(new int []{nums1[i],nums2[j]});
            }else{
                int [] root = maxHeap.peek();
                if(root[0] + root[1] > nums1[i] + nums2[j]){
                    maxHeap.poll();
                    maxHeap.offer(new int []{nums1[i],nums2[j]});
                }
            }
        }
    }
    List<List<Integer>> result = new LinkedList<>();
    while(!maxHeap.isEmpty()){
        int [] vals = maxHeap.poll();
        result.add(Arrays.asList(vals[0],vals[1]));
    }
    return result;
}

使用最小堆存储候选数对,每次得到一个最小数对

public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
    Queue<int []> minHeap = new PriorityQueue<>((p1,p2)->nums1[p1[0]] + nums2[p1[1]] - nums1[p2[0]] - nums2[p2[1]]);//存入的是下标,比较的是数对和的大小
    //存入下标为(0,0)~(k,0)的数对
    if(nums2.length > 0){
        for(int i = 0;i < Math.min(k,nums1.length);i++){
            minHeap.offer(new int[]{i,0});
        }
    }
    List<List<Integer>> result = new ArrayList<>();
    while(k-- > 0 && !minHeap.isEmpty()){
        int[] ids = minHeap.poll();
        result.add(Arrays.asList(nums1[ids[0]],nums2[ids[1]]));

        if(ids[1] < nums2.length - 1){
            minHeap.offer(new int[]{ids[0],ids[1]+1});
        }
    }
    return result;
}

小结

通常用最大堆来找出数据集合中的k个最小值,用最小堆来找出数据集合中的k个最大值