堆结构常见题
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)
解答:
法一:
-
思路
-
将数组中所有数转为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; }
-