如何手写一个堆?堆就是一个完全二叉树

完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。

对于最小堆,根节点一定小于左右子节点。但是左右子节点相互之间的大小不确定

堆 - 图1

  1. 向堆中插入一个数 heap[++size] = x; up(size);
  2. 求集合中的最小值 heap[1]
  3. 删除最小值 heap[1] = heap[size]; size--; down(1);
  4. 删除任意一个数x heap[k] = heap[size]; size--; down(k); up(k);
  5. 修改任意一个元素 heap[k] = x; up(k); down(k);

其中重要的一点:

堆 - 图2

堆排序

  1. import java.io.*;
  2. import java.util.Scanner;
  3. public class Main{
  4. static int n, m;
  5. static int[] heap = new int[100010];
  6. public static void main(String[] args){
  7. Scanner scan = new Scanner(System.in);
  8. String[] t = scan.nextLine().split(" ");
  9. n = Integer.parseInt(t[0]); m = Integer.parseInt(t[1]);
  10. for (int i = 1; i <= n; i++){
  11. heap[i] = scan.nextInt();
  12. }
  13. //从n/2开始down,这样也可以延伸到n
  14. for (int i = n / 2; i != 0; i --){
  15. down(i);
  16. }
  17. while (m-- != 0){
  18. System.out.print(heap[1] + " ");
  19. heap[1] = heap[n--];
  20. down(1);
  21. }
  22. }
  23. //维护一个堆
  24. public static void down(int idx){
  25. int tmp = idx;
  26. if (idx * 2 <= n && heap[idx * 2] < heap[tmp]) tmp = idx * 2;
  27. if (idx * 2 + 1 <= n && heap[idx * 2 + 1] < heap[tmp]) tmp = idx * 2 + 1;
  28. if (tmp != idx){
  29. int t = heap[tmp];
  30. heap[tmp] = heap[idx];
  31. heap[idx] = t;
  32. down(tmp);
  33. }
  34. }
  35. public static void up(int idx){
  36. int tmp = idx;
  37. if (idx / 2 >= 1 && heap[idx / 2] > heap[tmp]) tmp = idx / 2;
  38. if (tmp != idx){
  39. int t = heap[tmp];
  40. heap[tmp] = heap[idx];
  41. heap[idx] = t;
  42. up(tmp);
  43. }
  44. }
  45. }

大根堆/插排+二分53. 最小的k个数

大根堆

大根堆中是最符合条件的k个人。

从前往后遍历,前k个人一开始都是符合条件的。堆首A是最危险的,他很可能被踢掉。

进来一个人B,B < A, A被竞争走了。更换堆首C。从前到后扫描一遍

这样复杂度是nlogk+k

插排

从前向后遍历,每次二分找出第一个<=当前数的数,把当前数插入到这个数后面

复杂度(n, nlogn)+k

**但是移动的时候需要o(n) 就相当于o(n logn),而且要处理二分出左右边界的情况,不合适

思路是看y总视频的,手动模拟了栈,出了不少坑。

  1. 对堆进行排序,不要执行up操作

  2. 对结果集中每次加入堆首,不是heap[i]

  3. 堆的下沉操作,注意heap起始坐标一定是从1开始,到n/k结束,从0开始会导致堆首和1无法交换

    把0改成1,k-1没有改成k,导致一致出错

  1. 进行down操作的时候,t代表这个数最终的坐标,k代表这个数初始的坐标,因此是和heap[t]比较

    我开始写的是和k比较

  1. class Solution {
  2. int k;
  3. int[] heap;
  4. public List<Integer> getLeastNumbers_Solution(int[] input, int k) {
  5. if (input.length == 0) return new ArrayList<Integer>();
  6. this.k = k;
  7. heap = new int[k + 1];
  8. //堆中没有填满k个数,更新heap
  9. for(int i = 1; i <= k; i ++)
  10. heap[i] = input[i - 1];
  11. //对当前堆进行排序,不要执行up操作
  12. for (int i = k / 2; i >= 1; i--)
  13. down(i);
  14. //如果将剩下的数和堆首比较,如果<堆首,更新堆首,并使用down更新堆首
  15. for (int i = k; i < input.length; i ++){
  16. if (input[i] < heap[1]){
  17. //System.out.println(heap[1]);
  18. heap[1]=input[i];
  19. down(1);
  20. // System.out.println(heap[1]);
  21. }
  22. }
  23. List<Integer> res = new ArrayList<Integer>();
  24. //插入堆首到res中,将堆首和队尾进行交换,同时将堆个数-1,down堆首
  25. for (int i = 1; i <= k; i ++){
  26. //每次加入堆首,不是heap[i]
  27. res.add(heap[1]);
  28. int tmp = heap[1];
  29. heap[1] = heap[this.k];
  30. heap[this.k] = tmp;
  31. this.k--;
  32. down(1);
  33. }
  34. //结果就是res的反转
  35. Collections.reverse(res);
  36. return res;
  37. }
  38. //堆的下沉操作,注意heap起始坐标一定是从1开始,到n/k结束,从0开始会导致堆首和1无法交换
  39. //t代表这个数最终的坐标,k代表这个数初始的坐标,因此是和heap[t]比较
  40. public void down(int k){
  41. int t = k;
  42. if (2 * k <= this.k && heap[2*k] > heap[t]) t = 2 * k;
  43. if (2 *k + 1 <= this.k && heap[2*k + 1] > heap[t]) t = 2*k + 1;
  44. if (t != k){
  45. int tmp = heap[t];
  46. heap[t] = heap[k];
  47. heap[k] = tmp;
  48. down(t);
  49. }
  50. }
  51. }

两个堆构造两个相对大小的集合54. 数据流中的中位数

求中位数,如果能求出两个集合:

  1. 一个集合中s1的数都比另一个集合s2的数大或等
  2. 两个集合元素相差不超过1

中位数两边是 s1的最小值 & s2最大值

一个大根堆s2,一个小根堆s1恰好能满足这样的性质

  1. 先将数插入到大根堆。
  2. 如果两个堆之间出现了逆序元素(s1的堆首 < s2的堆首),交换堆首元素 满足性质1
  3. 如果两个堆之间元素个数相差>=2,将s1的堆首插入s2中。 满足性质2
  1. class Solution {
  2. PriorityQueue<Integer> s1 = new PriorityQueue();
  3. PriorityQueue<Integer> s2 = new PriorityQueue<>(( a, b)-> b - a);
  4. public void insert(Integer num) {
  5. //先把数插入到小根堆s1
  6. s1.offer(num);
  7. //维护性质1
  8. while (s2.size() > 0 && s1.peek() < s2.peek() ){
  9. Integer a1 = s1.poll();
  10. Integer a2 = s2.poll();
  11. s2.offer(a1); s1.offer(a2);
  12. }
  13. //维护性质2
  14. while (s1.size() > s2.size() + 1 ){
  15. s2.offer(s1.poll());
  16. }
  17. }
  18. public Double getMedian() {
  19. if (s1.size() != s2.size()) return s1.peek().doubleValue();
  20. else return (s1.peek() + s2.peek()) / 2.0;
  21. }
  22. }

最小堆+合并链表23. 合并K个升序链表

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode() {}
  7. * ListNode(int val) { this.val = val; }
  8. * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
  9. * }
  10. */
  11. class Solution {
  12. public ListNode mergeKLists(ListNode[] lists) {
  13. //最小堆+合并链表 o(logn * mn)
  14. //如果使用多路归并,o(mnn)
  15. // 插入对logn ,从堆中拿最小值,堆删除这个值,对应的数组下标后移一位,再加入这个值
  16. PriorityQueue<ListNode> heap = new PriorityQueue<>((a, b)->a.val - b.val);
  17. int n = lists.length;
  18. if (n == 0) return null;
  19. ListNode dummy = new ListNode(-1), pre = dummy;
  20. //加入前n个
  21. for (int i = 0; i < n; i ++)
  22. if (lists[i] != null) heap.add(lists[i]);
  23. while (!heap.isEmpty()){
  24. //取出最小元素
  25. ListNode num = heap.poll();
  26. if (num.next != null) heap.add(num.next);
  27. pre.next = num;
  28. pre = num;
  29. }
  30. return dummy.next;
  31. }
  32. }