堆结构常见题

堆结构常见题

1. 合并K个有序链表

测试链接:合并k个已排序的链表_牛客题霸_牛客网

描述

合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。

数据范围:节点总数 0≤n≤50000≤n≤5000,每个节点的val满足 ∣val∣<=1000∣val∣<=1000

要求:时间复杂度 O(nlogn)

示例1

输入:

[{1,2,3},{4,5,6,7}]

复制

返回值:

{1,2,3,4,5,6,7}

复制

示例2

输入:

[{1,2},{1,4,5},{6}]

复制

返回值:

{1,1,2,4,5,6}


解答:

  • 复杂度O(nlogk) n为总结点个数,k为链表条数 O(k)

  • 思路

    • 将每一条链表的头结点存入小根堆中

    • 每次从小根堆中弹出一个节点并挂在链表上, 并将弹出节点所在链表的下一个节点存入小根堆中

    • 当小根堆为空时结束循环

  • 代码

    • public class Code01_MergeKSortedLists {
      ​
          // 不要提交这个类
          public static class ListNode {
              public int val;
              public ListNode next;
          }
      ​
          // 提交以下的方法
          public static ListNode mergeKLists(ArrayList<ListNode> arr) {
              // 小根堆
              PriorityQueue<ListNode> heap = new PriorityQueue<>((a, b) -> a.val - b.val);
              for (ListNode h : arr) {
                  // 遍历所有的头!
                  if (h != null) {// 注意: 此处是if,若为while则会死循环
                      heap.add(h);// 将每一个头都加到小根堆里
                  }
              }// O(n)
              if (heap.isEmpty()) {// 如果所有链表数组都为空,则堆也为空,返回空
                  return null;
              }
              
              
              // 先弹出一个节点,做总头部
              ListNode h = heap.poll();
              ListNode pre = h;// pre指向表尾
              if (pre.next != null) {// 当有序链表还有下一个节点
                  heap.add(pre.next);// 将下一个节点放入堆中
              }
              
              
              while (!heap.isEmpty()) {// 当堆不为空
                  ListNode cur = heap.poll();// 弹出一个
                  pre.next = cur;// 链进链表
                  pre = cur;// 链表走到当前的位置
                  if (cur.next != null) {// cur所在链表还有下一个值
                      heap.add(cur.next);// 将下一个节点放入堆中
                  }
              }
              return h;
          }
      ​
      }

2. 最多线段重合问题

测试链接 : 线段重合_牛客题霸_牛客网 测试链接 : . - 力扣(LeetCode)

描述

每一个线段都有start和end两个数据项,表示这条线段在X轴上从start位置开始到end位置结束。

给定一批线段,求所有重合区域中最多重合了几个线段,首尾相接的线段不算重合。

例如:线段[1,2]和线段[2.3]不重合。

线段[1,3]和线段[2,3]重合

输入描述:

第一行一个数N,表示有N条线段

接下来N行每行2个数,表示线段起始和终止位置

输出描述:

输出一个数,表示同一个位置最多重合多少条线段

示例1

输入:

3
1 2
2 3
1 3

复制

输出:

2

复制

备注:

N≤104N≤104
1≤start,end≤1051≤start,end≤105

解答:

  • 时间复杂度: O(nlogn) 空间: O(n)

    • 每一个线段的尾巴都要进一次堆,出一次堆,每一次时间复杂度是O(logn),共O(nlogn)

    • 要有一个堆存放这些尾巴,共n条线段 O(n)

  • 思路

    • 由于线段重合部分一定是以某一个线段的开头为开头,因此,只需要计算出每一个线段开头时重合的个数,并返回最大值即可

      • 将所有数组中的线段按照的大小升序排序, 头大小相同的随机排

      • 遍历排序后的数组, 将每一个线段的加入小根堆

        • 先将小根堆中尾小于当前头的弹出

        • 将当前线段的加入小根堆

      • 返回小根堆中放入数据个数的最大值即为最多线段重合个数

  • 代码

    • import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.io.OutputStreamWriter;
      import java.io.PrintWriter;
      import java.io.StreamTokenizer;
      import java.util.Arrays;
      import java.util.PriorityQueue;
      ​
      public class Code02_MaxCover {
      ​
          public static int MAXN = 10001;
      ​
          public static int[][] line = new int[MAXN][2];// 用来保存所有线段
      ​
          public static int n;// 读入的有效线段个数
      ​
          public static void main(String[] args) throws IOException {
              BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
              StreamTokenizer in = new StreamTokenizer(br);
              
              PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
              while (in.nextToken() != StreamTokenizer.TT_EOF) {// 可能要读好多组
                  n = (int) in.nval;// 不要写成int n(局部变量)的形式,要同步到静态变量
                  for (int i = 0; i < n; i++) {
                      in.nextToken();
                      line[i][0] = (int) in.nval;// 每一个线段的头
                      in.nextToken();
                      line[i][1] = (int) in.nval;// 每一个线段的尾
                  }
                  out.println(compute());// 执行计算程序
              }
              out.flush();
              out.close();
              br.close();
          }
      ​
          public static int compute() {
              // 堆的清空(要计算好几组)
              size = 0;
      ​
              // 线段一共有n条,line[0...n-1][2] : {line[i][0] line[i][1]}, 左闭右闭
              // 所有线段,根据开始位置排序,结束位置无所谓
              
              // 比较器的用法
              // line [0...n) 排序 : 所有小数组,开始位置谁小谁在前
              Arrays.sort(line, 0, n, (a, b) -> a[0] - b[0]);
              int ans = 0;
              for (int i = 0; i < n; i++) {
                  // i : line[i][0] line[i][1]
                  while (size > 0 && heap[0] <= line[i][0]) {// 堆中还有数据且堆顶的数(最小右边界) <= 第i位的头
                      pop();// 弹出---->一直到堆中没有数据 或 堆中最小右边界能够冲入第i条线段
                  }
                  add(line[i][1]);// 将第i条线段的尾巴放入堆中
                  ans = Math.max(ans, size);// 计算已有的答案 与 当前堆数据总个数 的大小
              }
              return ans;// 返回最大值
          }
      ​
          // 小根堆,堆顶0位置
          public static int[] heap = new int[MAXN];
      ​
          // 堆的大小
          public static int size;
      ​
          public static void add(int x) {// heapInsert 向上建堆
              heap[size] = x;// 放入数据
              int i = size++;
              while (heap[i] < heap[(i - 1) / 2]) {// 向上看,直到自己的父亲不比自己小
                  swap(i, (i - 1) / 2);
                  i = (i - 1) / 2;
              }
          }
      ​
          public static void pop() {
              swap(0, --size);
              int i = 0, l = 1;// 最后一个值覆盖要弹出的最小值
              while (l < size) {// 还有左孩子 ---> heapify(向下调整堆)
                  int best = l + 1 < size && heap[l + 1] < heap[l] ? l + 1 : l;// 两个孩子中的最小值
                  best = heap[best] < heap[i] ? best : i;// 自己和孩子之间的最小值
                  if (best == i) {// 如果自己已经最小
                      break;// 停止
                  }
                  swap(i, best);// 否则自己位置的值和最小的孩子交换
                  i = best;// 更新自己的位置
                  l = i * 2 + 1;// 更新左孩子的位置
              }
          }
      ​
          public static void swap(int i, int j) {
              int tmp = heap[i];
              heap[i] = heap[j];
              heap[j] = tmp;
          }

3.将数组和减半的最少操作次数

测试链接 : . - 力扣(LeetCode)

image-20240806230518506

解答:

法一:

  • 思路

    • 将数组中所有数转为double放入大根堆中 (double: 保证每次作除后的精度都被保留)

      • 在放入大根堆的同时记录sum

    • 计算sum /= 2

    • 开始减

      • 从大根堆中每次弹出一个数并/2,, 除后的数再次放入堆中,并加入minus, ans(次数)++, 继续循环,直至minus >= sum

  • 代码

    • public class Code03_MinimumOperationsToHalveArraySum {
      ​
          // 提交时把halveArray1改名为halveArray
          public static int halveArray1(int[] nums) {
              // 大根堆  不要忘了泛型
              PriorityQueue<Double> heap = new PriorityQueue<>((a, b) -> b.compareTo(a));
              double sum = 0;
              for (int num : nums) {// 比普通for循环要快
                  heap.add((double) num);//将数组中的每一个数都转为double放入heap中 
                  sum += num;// 计算原来的总和
              }
              sum /= 2;// 要减少的目标值
              int ans = 0;// 减少的次数
              for (double minus = 0, cur; minus < sum; ans++, minus += cur) {// minus: 已减的数量
                  cur = heap.poll() / 2;// 当前弹出的最大值减半,是队列的写法: poll
                  heap.add(cur);// 将减半后的值放入堆中
              }
              return ans;// 返回操作次数
          }

法二:

  • 思路

    • 自己用数组做一个堆

      • 将所有的数 × 2^20,转为long型,放入堆中(为了自己作出一个能多次/2的精度,可按需调整)

      • 在放入大根堆的同时记录sum 该步骤必须要在heapify之前,否则0位置的数就不是要进行处理的数

      • 将放好的堆调向下调整为大根堆

    • 计算sum/=2

    • 开始减

      • 将0号位置(最大值)进行自除运算

      • minus加上0位置的值(已变为先前的一半) 该步骤必须要在heapify之前,否则0位置的数就不是要进行处理的数

      • 重新调整堆(0号位置已改变)

  • 代码

    •     public static int MAXN = 100001;
      ​
          public static long[] heap = new long[MAXN];
      ​
          public static int size;
      ​
          // 提交时把halveArray2改名为halveArray
          public static int halveArray2(int[] nums) {
              size = nums.length;
              long sum = 0;
              for (int i = size - 1; i >= 0; i--) {// 从底建堆
                  heap[i] = (long) nums[i] << 20;// 将每一个数*2^20 -> 转为long -> 放入堆中
                  sum += heap[i];// 要放在heapify之前,否则这个数就找不到了
                  heapify(i);// 调整为大根堆
              }
              sum /= 2;// 要减的目标值
              int ans = 0;// 已减的次数
              for (long minus = 0; minus < sum; ans++) {
                  heap[0] /= 2;// 等于法1中减完之后放回堆
                  minus += heap[0];// 要放在heapify之前,否则这个数就找不到了
                  heapify(0);// 重新调整堆
              }
              return ans;
          }
      ​
          public static void heapify(int i) {
              int l = i * 2 + 1;
              while (l < size) {
                  int best = l + 1 < size && heap[l + 1] > heap[l] ? l + 1 : l;
                  best = heap[best] > heap[i] ? best : i;
                  if (best == i) {
                      break;
                  }
                  swap(best, i);
                  i = best;
                  l = i * 2 + 1;
              }
          }
      ​
          public static void swap(int i, int j) {
              long tmp = heap[i];
              heap[i] = heap[j];
              heap[j] = tmp;
          }

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值