Ruinique 的算法刷题笔记——优先队列

堆排序

堆一般指的是二叉堆,顾名思义,二叉堆是完全二叉树或者近似完全二叉树。在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。

我们要实现堆排,就得将无序序列建成一个堆并且在输出栈顶元素后调整剩余元素使之满足最大堆的结构即可。

那么我们直接将每次最后一个元素先排好,然后再对剩余的元素做最大堆调整即可。

在 Java 中,优先队列可以看作一个最小堆(优先级是可以自定义)。

TopK 问题

对于简单的 TopK 问题,我们只需要维护一个最小堆就行了,如下:

数组中的第K个最大元素
给定整数数组 nums和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

  class Solution {
      public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> q = new PriorityQueue();
        for(int i:nums){
          if(q.size() == k){
            if(i > q.peek()){
              q.poll();
              q.offer(i);
            }
          }
          else{
            q.offer(i);
          }
        }
        return q.peek();
      }
  }

当然这种写法相对而言过于简单了,面试往往需要我们手动去建堆,我们就可以这么写(搬自官方题解评论区,我自己手搓的不如这个写的好,代码全聚在一坨):

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int heapSize = nums.length;
        buildMaxHeap(nums, heapSize);
        //建堆完毕后,nums【0】为最大元素。逐个删除堆顶元素,直到删除了k-1个。
        for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {
            //先将堆的最后一个元素与堆顶元素交换,由于此时堆的性质被破坏,需对此时的根节点进行向下调整操作。
            swap(nums, 0, i);
            //相当于删除堆顶元素,此时长度变为nums.length-2。即下次循环的i
            --heapSize;
            maxHeapify(nums, 0, heapSize);
        }
        return nums[0];
    }

    public void buildMaxHeap(int[] a, int heapSize) {
        //从最后一个父节点位置开始调整每一个节点的子树。数组长度为heasize,因此最后一个节点的位置为heapsize-1,所以父节点的位置为heapsize-1-1/2。
        for (int i = (heapSize-2)/ 2; i >= 0; --i) {
            maxHeapify(a, i, heapSize);
        } 
    }

    public void maxHeapify(int[] a, int i, int heapSize) {      //调整当前结点和子节点的顺序。
        //left和right表示当前父节点i的两个左右子节点。
        int left = i * 2 + 1, right = i * 2 + 2, largest = i;
        //如果左子点在数组内,且比当前父节点大,则将最大值的指针指向左子点。
        if (left < heapSize && a[left] > a[largest]) {
            largest = left;
        } 
        //如果右子点在数组内,且比当前父节点大,则将最大值的指针指向右子点。
        if (right < heapSize && a[right] > a[largest]) {
            largest = right;
        }
        //如果最大值的指针不是父节点,则交换父节点和当前最大值指针指向的子节点。
        if (largest != i) {
            swap(a, i, largest);
            //由于交换了父节点和子节点,因此可能对子节点的子树造成影响,所以对子节点的子树进行调整。
            maxHeapify(a, largest, heapSize);
        }
    }

    public void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

这里有一道类似的例题,只不过权值需要自己用哈希表去表示

前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
      HashMap<Integer,Integer> occur = new HashMap();
      for(int i:nums){
          int val = occur.containsKey(i) ? occur.get(i) + 1 : 1;
          occur.put(i,val);
      }

      PriorityQueue<Integer> q = new PriorityQueue(
        (o1,o2) -> occur.get(o1) - occur.get(o2)
      );

      for(int i:occur.keySet()){
        if(q.size() < k){
          q.offer(i);
        }
        else{
          if(occur.get(i) > occur.get(q.peek())){
            q.poll();
            q.offer(i);
          }
        }
      }

      int[] ret = new int[k];

      for(int i = 0;i < k;i++){
        ret[i] = q.poll();
      }

      return ret;
    }
}

维护对顶堆

当然,我们也可以利用 优先队列 实现一个对顶堆实现这题,不过对于对顶堆,我们有个好的例题:

数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 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 以内的答案将被接受。

我们只要构造一个大根堆,一个小根堆,就能求出一串数据中间的内容:

class MedianFinder {

    public PriorityQueue<Integer> less;

    public PriorityQueue<Integer> greater;

    public double median;

    public MedianFinder() {
      less = new PriorityQueue<Integer>(
        (o1 , o2) -> o2 - o1
      );
      greater = new PriorityQueue<Integer>(
        (o1 , o2) -> o1 - o2
      );
    }


    public void addNum(int num) {
      if(less.size() == 0){
        less.offer(num);
        median = num;
      }
      else{
        if(less.size() > greater.size()){
          if(num >= median){
            greater.offer(num);
          }
          else{
            greater.offer(less.poll());
            less.offer(num);
          }
          median = (greater.peek() + less.peek()) / 2.0;
        }
        else{
          if(num >= median){
            greater.offer(num);
            less.offer(greater.poll());
          }
          else{
            less.offer(num);
          }
            median = less.peek();
        }
      }
    }

    public double findMedian() {
      return median;
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

多维数据和条件的利用

有的时候我们会遇上一个结构拥有多维的特征,或者结构上的有序性,这时候我们只需要关于其中需要我们对其进行排序的性质建立优先队列即可,如下:

查找和最小的 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]

class Solution {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
      ArrayList<List<Integer>> ret = new ArrayList();
      PriorityQueue<int[]> q = new PriorityQueue<int[]>(
        (o1,o2) -> -(nums1[o2[0]] + nums2[o2[1]]) + (nums1[o1[0]] + nums2[o1[1]] )
      );
      for(int i = 0;i < nums1.length && i < k;i++){
        q.offer(new int[]{i,0});
      }   
      for(int i = 0;i < k;i++){
        if(q.size() == 0) break;
        int[] pair = q.poll();
        ret.add(List.of(nums1[pair[0]],nums2[pair[1]]));
        if(pair[1] + 1 < nums2.length)
          q.offer(new int[]{pair[0],pair[1] + 1});
      }
      return ret;
    }
}

这题就要求我们查找和最小,同时还有着两个数组非递减排序的特点,所以我们充分利用对应特点进行剪枝。

IPO
假设 力扣(LeetCode)即将开始 IPO 。为了以更高的价格将股票卖给风险投资公司,力扣 希望在 IPO 之前开展一些项目以增加其资本。 由于资源有限,它只能在 IPO 之前完成最多 k 个不同的项目。帮助 力扣 设计完成最多 k 个不同项目后得到最大总资本的方式。
给你 n 个项目。对于每个项目 i ,它都有一个纯利润 profits[i] ,和启动该项目需要的最小资本 capital[i] 。
最初,你的资本为 w 。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。
总而言之,从给定项目中选择 最多 k 个不同项目的列表,以 最大化最终资本 ,并输出最终可获得的最多资本。
答案保证在 32 位有符号整数范围内。

而 IPO 这题我刚开始没有对数组进行预处理:

class Solution {
    public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
// 每一刻我都要选最能盈利的
      int ret = w;
// 利润的大根堆
      PriorityQueue<Integer> profitsQueue = new PriorityQueue<Integer>(
        (o1,o2) -> profits[o2] - profits[o1]
      );
      boolean[] ok = new boolean[profits.length];
      for(int i = 0;i < k;i++){
        for(int j = 0;j < capital.length;j++){
          if(!ok[j] && capital[j] <= ret){
            profitsQueue.offer(j);
            ok[j] = true;
          }
        }
      if(profitsQueue.size() == 0) return ret;
      ret += profits[profitsQueue.poll()];
      }
      return ret;
    }
}

这么写以后不仅多开了 ok 这个 boolean 数组,复杂度还是 kn + (k + n)logn。

所以我们得先排序做预处理,如下

class Solution {
    public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
      int ret = w;
      int[][] project = new int[profits.length][2];
      for(int i = 0;i < project.length;i++){
        project[i][0] = capital[i];
        project[i][1] = profits[i];
      }
      Arrays.sort(project,(o1,o2) -> o1[0] - o2[0]);
      PriorityQueue<Integer> q = new PriorityQueue<Integer>(
        (o1,o2) -> project[o2][1] - project[o1][1]
      );
      int index = 0;
      while(k-- != 0){
        while(index < project.length && project[index][0] <= ret){
          q.offer(index);
          index++;
        }
        if(q.isEmpty())
          return ret;
        else
          ret += project[q.poll()][1];
      }
      return ret;
    }
}
posted on 2023-09-30 21:11  Ruinique  阅读(28)  评论(0)    收藏  举报