适用题型

堆结构适用于求TopK相关的问题,
求前K个大的数采用固定堆大小为K的小根堆,每次移除堆顶的最小元素,到最后剩下的就是该组数列中最大的K的数字。
同理,求TopK小的的问题,采用大根堆,每次堆顶元素大的出堆,剩下的就是最小的K个数字。


堆结构-完全二叉树

这里是大根堆结构
heapInsert(int[] arr, int index) 时间复杂度O(logN) 上移过程:将指定位置index的数向上调整,添加节点时使用 时间复杂度O(logN)

  1. // 新加进来的数,现在停在了index位置,向上调整,使其最后保持堆的结构特性
  2. // 移动到0位置,或者干不掉自己的父亲了,停!
  3. private void heapInsert(int[] arr, int index) {
  4. // [index] < [index-1]/2 当前节点小于父节点,或者到了堆顶结束
  5. // index == 0
  6. // 这个条件包含以上两种终止情况,当index=0时,条件是不成立的。
  7. while (arr[index] > arr[(index - 1) / 2]) {
  8. swap(arr, index, (index - 1) / 2);
  9. index = (index - 1) / 2;
  10. }
  11. }

heapify(int[] arr, int index, int heapSize) 时间复杂度O(logN) 下沉过程:将指定位置index的数向下调整,删除节点时,使用,从堆顶开始向下调整

  1. // 从index位置,开始向下调整,最后保持堆的结构特性
  2. // 停:较大的孩子都不再比index位置的数大或者已经没孩子时终止,到了heapsize。
  3. private void heapify(int[] arr, int index, int heapSize) {
  4. int left = index * 2 + 1; // 计算出左孩子小标
  5. while (left < heapSize) { // 如果有左孩子,继续向下执行/
  6. // 获取较大孩子的下标
  7. int largest = ( left + 1 < heapSize && arr[left + 1] ) > arr[left] ? left + 1 : left; // 如果有右孩子,且右孩子的值大于左孩子的值,将右孩子下表个largest,否则左孩子下标给largest
  8. int maxChildIndex = arr[largest] > arr[index] ? largest : index; // 比较之后较大的下标
  9. // 如果父节点》=子节点大,不需要调整,跳出。
  10. if (maxIndex == index) {
  11. break;
  12. } else {
  13. // index位置的数和较大孩子要互换
  14. swap(arr, largest, index);
  15. index = maxChildIndex;
  16. left = index * 2 + 1;
  17. }
  18. }
  19. }

例题1:假设在一个正常的大根堆中,突然由于某种原因,某个位置i上的数字X突然发生了改变->X^,但是不知道是变大了还是变小了,如何快速的调整X^,使其仍然保持为大根堆的结构。

对i位置的数,顺序执行heapInsert以及heapify两个过程,即可完成调整,时间复杂度为O(logN)

堆结构(大根堆)的相关操作代码如下:

  1. public class Code_Heap {
  2. // 手写大堆根结构,如果需要小根堆,只需要更改比较器即可,定义小的在上边,大的在下面。
  3. public static class MyMaxHeap {
  4. private int[] heap;
  5. private final int limit;
  6. private int heapSize;
  7. public MyMaxHeap(int limit) {
  8. heap = new int[limit];
  9. this.limit = limit;
  10. heapSize = 0;
  11. }
  12. public boolean isEmpty() {
  13. return heapSize == 0;
  14. }
  15. public boolean isFull() {
  16. return heapSize == limit;
  17. }
  18. public void push(int value) {
  19. if (heapSize == limit) {
  20. throw new RuntimeException("heap is full");
  21. }
  22. heap[heapSize] = value;
  23. // value heapSize
  24. heapInsert(heap, heapSize++);
  25. }
  26. // 用户此时,让你返回最大值,并且在大根堆中,把最大值删掉
  27. // 剩下的数,依然保持大根堆结构
  28. public int pop() {
  29. int ans = heap[0];
  30. swap(heap, 0, --heapSize);
  31. heapify(heap, 0, heapSize);
  32. return ans;
  33. }
  34. // 新加进来的数,现在停在了index位置,向上调整,使其最后保持堆的结构特性
  35. // 移动到0位置,或者干不掉自己的父亲了,停!
  36. private void heapInsert(int[] arr, int index) {
  37. // [index] [index-1]/2
  38. // index == 0
  39. while (arr[index] > arr[(index - 1) / 2]) { // 这个条件包含以上两种终止情况,当index=0时,条件是不成立的。
  40. swap(arr, index, (index - 1) / 2);
  41. index = (index - 1) / 2;
  42. }
  43. }
  44. // 从index位置,开始向下调整,最后保持堆的结构特性
  45. // 停:较大的孩子都不再比index位置的数大或者已经没孩子时终止
  46. private void heapify(int[] arr, int index, int heapSize) {
  47. int left = index * 2 + 1; // 计算出左孩子小标
  48. while (left < heapSize) { // 如果有左孩子,继续向下执行/
  49. // 获取较大孩子的下标
  50. int largest = ( left + 1 < heapSize && arr[left + 1] ) > arr[left] ? left + 1 : left; // 如果有右孩子,且右孩子的值大于左孩子的值,将右孩子下表个largest,否则左孩子下标给largest
  51. int maxIndex = arr[largest] > arr[index] ? largest : index; // 比较之后较大的下标
  52. // 如果父节点》=子节点大,不需要调整,跳出。
  53. if (maxIndex == index) {
  54. break;
  55. } else {
  56. // index位置的数和较大孩子要互换
  57. swap(arr, largest, index);
  58. index = maxIndex;
  59. left = index * 2 + 1;
  60. }
  61. }
  62. }
  63. private void swap(int[] arr, int i, int j) {
  64. int tmp = arr[i];
  65. arr[i] = arr[j];
  66. arr[j] = tmp;
  67. }
  68. }
  69. public static class MyComparator implements Comparator<Integer>{
  70. @Override
  71. public int compare(Integer o1, Integer o2) { // compare(o1,o2) 返回值 为正 o1>o2 为负 o1<o2 0 相等
  72. return o2 - o1; //这种定义实现的是小根堆 小的优先,举例参数具体值带入即可明白
  73. }
  74. }
  75. public static void main(String[] args) {
  76. // 小根堆
  77. PriorityQueue<Integer> heap = new PriorityQueue<>(new MyComparator());
  78. heap.add(5);
  79. heap.add(5);
  80. heap.add(5);
  81. heap.add(3);
  82. // 5 , 3
  83. System.out.println(heap.peek());
  84. heap.add(7);
  85. heap.add(0);
  86. heap.add(7);
  87. heap.add(0);
  88. heap.add(7);
  89. heap.add(0);
  90. System.out.println(heap.peek());
  91. while(!heap.isEmpty()) {
  92. System.out.println(heap.poll());
  93. }
  94. }
  95. }

堆排序代码实现

堆排序步骤:
(1)先让整个数组变成大根堆的结构,建大根堆有两种形式。

  - 自上向下建堆, O(N*logN)
  - 自下向上建堆, O(N)

(2)依次进行heapify,逻辑删除堆顶元素(也就是每次的全局最大值),得出一个有序序列。O(N*logN)

5. 堆排序及相关算法 - 图1

// 堆排序额外空间复杂度O(1)
public static void heapSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }

    //建大根堆方式1: O(N*logN)  从数组小下标开始建根   自上而下 heapinseart
//        for (int i = 0; i < arr.length; i++) { // O(N)
//            heapInsert(arr, i);                // O(logN)
//        }

    //建大根堆方式2: O(N)       从数组大头开始建根   自下而上 heapify
    for (int i = arr.length - 1; i >= 0; i--) {
        heapify(arr, i, arr.length);
    }

    int heapSize = arr.length;  // 大根堆构建完成之后,开始进行排序
    swap(arr, 0, --heapSize);   // 堆顶元素(最大值)和最后一个值交换,并且堆大小减一,全局最大值和堆断连。  相当于逻辑删除堆中的堆顶元素。
    // O(N*logN)
    while (heapSize > 0) { // O(N)
        heapify(arr, 0, heapSize); // O(logN) 将去除全局最大值后的堆结构重新进行heapify,确定剩下的全局最大值
        swap(arr, 0, --heapSize); // O(1)     依次断连,堆顶最大值元素,然后再heapify,维持大根堆结构
    }
    // 最后数组中剩下的就是拍好序的数组序列了。
}
public static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

加强堆(优化)

提出问题:
对于现在有一个需求,对于一个大根堆,堆元素存放在数组中。现在要删除堆中一个指定元素的位置,普通的堆结构是先遍历一遍堆数组,找到这个元素的位置,这个过程是O(N)的复杂度,然后再进行heapify,O(logN),整个过程的时间复杂度是O(N)
能否做到删除堆中的任意元素为log(N)的复杂度呢?
在JavaApi中提供的堆操作中,是无法实现O(logN)的复杂度的。因为在PriorityQueue源码中只有一个数组来存放堆元素,也就是给定一个数组下标索引,我们能够立刻找到该位置的数,这就是所谓的正向索引。但是,给定一个元素,能立刻找到该元素所在的位置吗,在当前情况下显然是不能的,因为没有存反向索引,也就是不能通过一个具体值来立刻确定其所在的位置,反向索引的具体实现是具体值作为索引值,具体值的位置作为value值。这样就可以在O(1)的时间复杂度内确定给定元素所在的位置。那么删除堆中元素的时间复杂度就变成了O(logN)
即重构堆的实现,自己添加反向索引表,能够将时间代价由 O(N) —-> O(logN)
加强堆 代码实现如下:

/*
 * T一定要是非基础类型,有基础类型需求包一层
 */
public class HeapGreater<T> {

    private ArrayList<T> heap;             // 正向索引,动态数组存储堆元素
    private HashMap<T, Integer> indexMap;  // 反向索引表,key存元素值,value存放元素所在的位置  get(key)的时间复杂度为O(1)
    private int heapSize;
    private Comparator<? super T> comp;

    public HeapGreater(Comparator<T> c) {
        heap = new ArrayList<>();
        indexMap = new HashMap<>();
        heapSize = 0;
        comp = c;
    }

    public void remove(T obj) {
        T replace = heap.get(heapSize - 1);
        int index = indexMap.get(obj);
        indexMap.remove(obj);
        heap.remove(--heapSize);
        if (obj != replace) {
            heap.set(index, replace);
            indexMap.put(replace, index);
            resign(replace);
        }
    }

    // 堆元素值发生变化进行调整
    public void resign(T obj) {
        heapInsert(indexMap.get(obj));
        heapify(indexMap.get(obj));
    }

    // 请返回堆上的所有元素
    public List<T> getAllElements() {
        List<T> ans = new ArrayList<>();
        for (T c : heap) {
            ans.add(c);
        }
        return ans;
    }

    private void heapInsert(int index) {
        while (comp.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
            swap(index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    private void heapify(int index) {
        int left = index * 2 + 1;
        while (left < heapSize) {
            int best = left + 1 < heapSize && comp.compare(heap.get(left + 1), heap.get(left)) < 0 ? (left + 1) : left;
            best = comp.compare(heap.get(best), heap.get(index)) < 0 ? best : index;
            if (best == index) {
                break;
            }
            swap(best, index);
            index = best;
            left = index * 2 + 1;
        }
    }

    public boolean isEmpty() {
        return heapSize == 0;
    }

    public int size() {
        return heapSize;
    }

    public boolean contains(T obj) {
        return indexMap.containsKey(obj);
    }

    public T peek() {
        return heap.get(0);
    }

    public void push(T obj) {
        heap.add(obj);
        indexMap.put(obj, heapSize);
        heapInsert(heapSize++);
    }

    public T pop() {
        T ans = heap.get(0);
        swap(0, heapSize - 1);
        indexMap.remove(ans);
        heap.remove(--heapSize);
        heapify(0);
        return ans;
    }

    private void swap(int i, int j) {
        T o1 = heap.get(i);
        T o2 = heap.get(j);
        heap.set(i, o2);
        heap.set(j, o1);
        indexMap.put(o2, i);
        indexMap.put(o1, j);
    }

}

题目1. 最大线段重合问题

给定很多线段,每个线段都有两个数[start, end],表示线段开始位置和结束位置,左右都是闭区间,规定如下 1)线段的开始和结束位置都是整数值 2)线段重合区域的长度必须>=1 返回线段最多重合区域中,包含了几条线段。

解题思路:

  • 线段的最大重合区域的左边界,一定是某条线段的左边界
  • 对每个线段的开始位置进行排序
  • 求每个开始位置的最大贯穿数就是最大重合的线段数
  • 用小根堆来保存线段的结尾位置数
  • 按线段的头排序然后依次和小根堆堆顶元素进行比较,如果 当前线段的头>小根堆堆顶,弹出,直到堆顶元素>当前线段开始元素,将当前线段尾部加入到堆中,堆中元素的个数就是该位置向右贯穿的线段的个数。

Eg: 线段如下 【1,3】【2,4】【2,6】【3,4】 【5,8】【7,9】
最大重合区域为:3-4和2-3,包含线段数为3条
微信图片_20220102130537.jpg-=

暴力笨蛋方法:

1、从所有线段中找到开始位置的最小值min 和结束位置的最大值max。所有的线段将坐落在【min,max】这个区域。设min = 1 max = 10
2、重合区域必定包含某个 x.5 因此可以在【1,10】上设点 1.5 、 2.5、3.5、4.5、5.5、6.5、7.5、8.5、9.5。
然后统计每个点所属的线段个数,统计结果的最大值即是答案。
时间复杂度 : O((max - min) * N) N 表示线段数量

小根堆方法:

每个线段根据开始位置,从小到大排个序。
准备一个小根堆
eg: 【1,3】【2,4】【2,6】【3,4】 【5,8】【7,9】
任何一个重合区域的左边界,必定是某个线段的左边界。
求每一个线段的左边界向右贯穿线段的最大数量。
如上几个线段的例子:
以1开始向右的最大贯穿数为 1
以2开始向右的最大贯穿数为 3
以3开始向右的最大贯穿数为 3
代码实现

    /**
     *    实现方式1:暴力笨蛋方法
     */
    public static int maxCover1(int[][] lines) {
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < lines.length; i++) {
            min = Math.min(min, lines[i][0]);// 所有线段的开始位置求最小值
            max = Math.max(max, lines[i][1]);// 所有线段的结束位置求最大值
        }
        int cover = 0;
        // 遍历每一个点5被多少个线段包含
        for (double p = min + 0.5; p < max; p += 1) {
            int cur = 0;
            for (int i = 0; i < lines.length; i++) {
                if (lines[i][0] < p && lines[i][1] > p) {
                    cur++;
                }
            }
            cover = Math.max(cover, cur);
        }
        return cover;
    }

    /**
     * 实现方式2: 小根堆 实现
     * 时间复杂度  O(N * logN)  最差情况:每个线段的结尾进一次小根堆,出一次小根堆。
     */
    public static int maxCover2(int[][] m) {
        Line[] lines = new Line[m.length];
        for (int i = 0; i < m.length; i++) {
            lines[i] = new Line(m[i][0], m[i][1]);
        }
        Arrays.sort(lines, new StartComparator()); // 按照线段开始位置排序
        // 小根堆,每一条线段的结尾数值,使用默认的
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        int max = 0;
        for (int i = 0; i < lines.length; i++) {
            // lines[i] -> cur  在黑盒中,把<=cur.start 东西都弹出
            while (!heap.isEmpty() && heap.peek() <= lines[i].start) {
                heap.poll();
            }
            // 将线段的结尾位置加到小根堆中
            heap.add(lines[i].end);
            // 更新max
            max = Math.max(max, heap.size());
        }
        return max;
    }

    // 定义线段类
    public static class Line {
        public int start;
        public int end;
        public Line(int s, int e) {
            start = s;
            end = e;
        }
    }

    // 定义比较器
    public static class EndComparator implements Comparator<Line> {
        @Override
        public int compare(Line o1, Line o2) {
            return o1.end - o2.end;
        }
    }

题目2:手动改写堆相关题目(重要)加强堆

Java中提供的堆相关的API,PriorityQueue提供的操作是有限的,相对使用者来说就相当于一个黑盒,给指定的序列构建堆,返回堆顶元素等操作。

当一组对象数据通过PriorityQueue建立起堆以后,如果对象的内部比较属性突然改变,正常操作我们需要一个heapInseart或者heapipy过程,但是ProorityQueue却不支持这种操作,此时这个堆就成了一个无效堆。

原因:PriorityQueue没有提供反向索引表。

堆的存储采用数组结构
假设内存对象a(5)表示对象a,内部比较属性为5,存在数组0位置,如上,b(7)在数组1位置,c(8)在数组2位置。
数组中只能通过数组下表(可以认为是索引)找到具体的对象,但是如果a()对象中的比较属性发生变化,只能通过遍历整个数组来找到a()的位置,来确定是否来调整堆结构,这个过程是O(N)的,但是通过heapInseart和heapify两个过程来调整堆结构是O(logN)级别的。


题目:电商用户抽奖策略
给定一个整型数组int[] arr;和一个布尔类型数组boolean[] op
两个数组一定等长,假设长度为N,arr[i]表示客户编号,op[i]表示客户操作
arr = [3, 3, 1, 2, 1, 2, 5 …]
op = [T, T, T, T, F, T, F …]
依次表示:3用户购买了一件商品,3用户购买了一件商品,1用户购买了一件商品,2用户购买了一件商品,
1用户退货了一件商品,2用户购买了一件商品,5用户退货了一件商品…
一对arr[i]和po[i]就代表一个事件:
用户号为arr[i],op[i] == T代表这个用户购买了一件商品,op[i]==F代表这个用户退货了一件商品,现在作为一名电商平台负责人,想在每一个事件到来的时候,都给购买次数最多的前K名用户颁奖。所以,每个事件发生后,都需要一个得奖名单(得奖区)。
得奖系统的规则如下

  1. 如果某个用户购买的商品数为0,但是又发生了退货事件,则认为该事件无效,得奖名单和之前事件时保持一致,如例子中的5用户
  2. 某用户发生购买商品事件,购买商品数+1;发生退货事件,购买商品数-1
  3. 每次都是最多K个用户获奖,K也为传入的参数,如果根据全部规则,得奖人数确实不够K个,那就以不够的情况为输出结果。
  4. 得奖系统分为得奖区和候选区,任何用户只要购买数>0,一定在这两个区域中的一个
  5. 购买数量最大的前K名用户进入得奖区,在最初时如果得奖区没有达到K个用户,那么新来的用户直接进入到得奖区
  6. 如果购买数量不足以进入得奖区的用户,直接进入到候选区
  7. 如果候选区购买数最多的用户,已经足以进入到得奖区,该用户就会替换得奖区中购买数量最少的用户(大于才能替换);

如果得奖区中购买数最多的用户有多个,就替换最早进入到得奖区的用户
如果候选区中购买数最多的用户有多个,机会给最早进入到候选区的用户

  1. 候选区和得奖区是两套时间,

    因用户只会在其中一个区域,所以只会有一个区域的时间,另一个区域没有;<br />从得奖区出来进入候选区的用户,得奖区时间删除,进入候选区的时间就是当前事件的时间;<br />从候选区出来进入得奖区的用户,候选区时间删除,进入得奖区的时间就是当前时间的时间;
    
  2. 如果某用户购买数为0,不管在哪个区域都离开,区域时间删除,离开是指彻底离开,任何区域也找不到该用户

如果下次该用户又发生购买行为,产生>0的购买数,会再次根据之前规则回到某个区域中,进入区域的时间重新计算


package class07;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

public class Code02_EveryStepShowBoss {

    // 用户类
    public static class Customer {
        public int id;  // 用户编号
        public int buy; // 购买商品数量
        public int enterTime; // 进入区域的时间

        public Customer(int v, int b, int o) {
            id = v;
            buy = b;
            enterTime = 0;
        }
    }

    // 候选区比较器: 候选区购买数量相同,最早进入候选区的用户排在前面(优先进入得奖区)
    public static class CandidateComparator implements Comparator<Customer> {
        @Override
        public int compare(Customer o1, Customer o2) {
            return o1.buy != o2.buy ? (o2.buy - o1.buy) : (o1.enterTime - o2.enterTime);
        }
    }

    // 得奖区比较器:得奖区购买数量相同,最早进入候选区的用户排在前面(优先被替换)
    public static class DaddyComparator implements Comparator<Customer> {
        @Override
        public int compare(Customer o1, Customer o2) {
            return o1.buy != o2.buy ? (o1.buy - o2.buy) : (o1.enterTime - o2.enterTime);
        }
    }

    /**
     * 得奖区结构
     */
    public static class WhosYourDaddy {

        private HashMap<Integer, Customer> customers;  // 用户编号
        private HeapGreater<Customer> candHeap;  // 候选区的堆
        private HeapGreater<Customer> daddyHeap;  //得奖区的堆
        private final int daddyLimit;

        public WhosYourDaddy(int limit) {
            customers = new HashMap<Integer, Customer>();
            candHeap = new HeapGreater<>(new CandidateComparator());
            daddyHeap = new HeapGreater<>(new DaddyComparator());
            daddyLimit = limit;
        }

        // 当前 处理i号事件,arr[i] -> id,  buyOrRefund

        /**
         * 处理当前事件
         * @param time 时间点
         * @param id  用户编号
         * @param buyOrRefund 用户操作行为
         */
        public void operate(int time, int id, boolean buyOrRefund) {
            if (!buyOrRefund && !customers.containsKey(id)) {
                return;
            }
            if (!customers.containsKey(id)) {
                customers.put(id, new Customer(id, 0, 0));
            }
            Customer c = customers.get(id);
            if (buyOrRefund) {
                c.buy++;
            } else {
                c.buy--;
            }
            if (c.buy == 0) {
                customers.remove(id);
            }
            if (!candHeap.contains(c) && !daddyHeap.contains(c)) { // 新用户
                if (daddyHeap.size() < daddyLimit) {
                    c.enterTime = time;
                    daddyHeap.push(c);
                } else {
                    c.enterTime = time;
                    candHeap.push(c);
                }
            } else if (candHeap.contains(c)) { // 老用户,且在候选区
                if (c.buy == 0) { // 购买数为0 删掉
                    candHeap.remove(c);
                } else {         // 购买数不是零,则必定发生变化。调用候选区小根堆的元素内容改变的方法,进行调整
                    candHeap.resign(c);
                }
            } else {  // 老用户,且在得奖区
                if (c.buy == 0) {  // 购买数为 0
                    daddyHeap.remove(c);
                } else {       // 购买数不是零,则必定发生变化。调用得奖区小根堆的元素内容改变的方法
                    daddyHeap.resign(c);
                }
            }

            // 候选区和得奖区交换 交换之前 候选区和得奖区一定要进行排序
            daddyMove(time);
        }

        /**
         * 返回得奖区的用户id列表
         * @return
         */
        public List<Integer> getDaddies() {
            List<Customer> customers = daddyHeap.getAllElements();
            List<Integer> ans = new ArrayList<>();
            for (Customer c : customers) {
                ans.add(c.id);
            }
            return ans;
        }

        // 候选区移动
        private void daddyMove(int time) {
            // 候选区为空
            if (candHeap.isEmpty()) {
                return;
            }
            // 候选区非空
            if (daddyHeap.size() < daddyLimit) {
                Customer p = candHeap.pop();
                p.enterTime = time;
                daddyHeap.push(p);
            } else {
                if (candHeap.peek().buy > daddyHeap.peek().buy) {
                    Customer oldDaddy = daddyHeap.pop();
                    Customer newDaddy = candHeap.pop();
                    oldDaddy.enterTime = time;
                    newDaddy.enterTime = time;
                    daddyHeap.push(newDaddy);
                    candHeap.push(oldDaddy);
                }
            }
        }

    }

    /**
     * 实现方式二:采用加强堆的形式 时间复杂度为 O(N * lgN)
     * @param arr
     * @param op
     * @param k
     * @return
     */
    public static List<List<Integer>> topK(int[] arr, boolean[] op, int k) {
        List<List<Integer>> ans = new ArrayList<>();
        WhosYourDaddy whoDaddies = new WhosYourDaddy(k); // 得奖系统的结构
        for (int i = 0; i < arr.length; i++) {
            whoDaddies.operate(i, arr[i], op[i]);  // 结构调整
            ans.add(whoDaddies.getDaddies());      // 返回得奖区
        }
        return ans;
    }


    /**
     * 实现方式一: 暴力方式解决  时间复杂度至少为为 O(N * N * lgN)
     *
     * @param arr 客户编号列表
     * @param op  客户操作列表
     * @param k   前K个用户获奖
     */
    public static List<List<Integer>> compare(int[] arr, boolean[] op, int k) {
        HashMap<Integer, Customer> map = new HashMap<>();  // 用户编号
        ArrayList<Customer> cands = new ArrayList<>();  // 候选区
        ArrayList<Customer> daddy = new ArrayList<>();  // 得奖区
        List<List<Integer>> ans = new ArrayList<>();    // 返回得奖结果{{客户编号1,购买数量1,发生时间1},{客户编号2,购买数量2,发生时间2}...}

        for (int i = 0; i < arr.length; i++) {
            int id = arr[i];
            boolean buyOrRefund = op[i];
            if (!buyOrRefund && !map.containsKey(id)) {  // 规则1.用户购买的商品数为0,但是又发生了退货事件
                ans.add(getCurAns(daddy));
                continue;
            }
            // 没有发生:用户购买数为0并且又退货了这个事件

            // 用户之前购买数是0,此时买货事件。新进来的用户
            // 用户之前购买数>0, 此时买货
            // 用户之前购买数>0,  此时退货
            if (!map.containsKey(id)) {
                map.put(id, new Customer(id, 0, 0)); // 购买数量 和 进入时间  暂时设置为0, 在后续操作中再进行调整赋值
            }
            //
            Customer c = map.get(id);
            if (buyOrRefund) {
                c.buy++;
            } else {
                c.buy--;
            }
            if (c.buy == 0) {
                map.remove(id);
            }
            // c
            // 如果是 新用户发生购买行为
            if (!cands.contains(c) && !daddy.contains(c)) {
                if (daddy.size() < k) {  // 得奖区未满,直接进入到得奖区,否则进入到候选区
                    c.enterTime = i;
                    daddy.add(c);
                } else {
                    c.enterTime = i;
                    cands.add(c);
                }
            }

            // 遍历处理购买数为0的数据
            cleanZeroBuy(cands);
            cleanZeroBuy(daddy);

            // 事件发生后排序
            cands.sort(new CandidateComparator());  // 排序时间复杂度: O(N * lnN)
            daddy.sort(new DaddyComparator());      // 排序时间复杂度: O(K * lnK)

            // 移动更新区域  排序后候选区的第一个和得奖区的第一个进行比较,看是否发生移动
            move(cands, daddy, k, i);
            ans.add(getCurAns(daddy));
        }
        return ans;
    }

    /**
     * 候选区和得奖区数据交换
     * @param cands
     * @param daddy
     * @param k
     * @param time
     */
    public static void move(ArrayList<Customer> cands, ArrayList<Customer> daddy, int k, int time) {
        // 候选区为空,不发生替换
        if (cands.isEmpty()) {
            return;
        }
        // 候选区不为空
        if (daddy.size() < k) {  // 得奖区没满
            Customer c = cands.get(0);
            c.enterTime = time;
            daddy.add(c);
            cands.remove(0);
        } else { // 得奖区满了,候选区有东西
            if (cands.get(0).buy > daddy.get(0).buy) {
                Customer oldDaddy = daddy.get(0);
                daddy.remove(0);
                Customer newDaddy = cands.get(0);
                cands.remove(0);
                newDaddy.enterTime = time;
                oldDaddy.enterTime = time;
                daddy.add(newDaddy);
                cands.add(oldDaddy);
            }
        }
    }

    public static void cleanZeroBuy(ArrayList<Customer> arr) {
        List<Customer> noZero = new ArrayList<Customer>();
        for (Customer c : arr) {
            if (c.buy != 0) {
                noZero.add(c);
            }
        }
        arr.clear();
        for (Customer c : noZero) {
            arr.add(c);
        }
    }

    public static List<Integer> getCurAns(ArrayList<Customer> daddy) {
        List<Integer> ans = new ArrayList<>();
        for (Customer c : daddy) {
            ans.add(c.id);
        }
        return ans;
    }

    // 为了测试
    public static class Data {
        public int[] arr;
        public boolean[] op;

        public Data(int[] a, boolean[] o) {
            arr = a;
            op = o;
        }
    }

    // 为了测试
    public static Data randomData(int maxValue, int maxLen) {
        int len = (int) (Math.random() * maxLen) + 1;
        int[] arr = new int[len];
        boolean[] op = new boolean[len];
        for (int i = 0; i < len; i++) {
            arr[i] = (int) (Math.random() * maxValue);
            op[i] = Math.random() < 0.5 ? true : false;
        }
        return new Data(arr, op);
    }

    // 为了测试
    public static boolean sameAnswer(List<List<Integer>> ans1, List<List<Integer>> ans2) {
        if (ans1.size() != ans2.size()) {
            return false;
        }
        for (int i = 0; i < ans1.size(); i++) {
            List<Integer> cur1 = ans1.get(i);
            List<Integer> cur2 = ans2.get(i);
            if (cur1.size() != cur2.size()) {
                return false;
            }
            cur1.sort((a, b) -> a - b);
            cur2.sort((a, b) -> a - b);
            for (int j = 0; j < cur1.size(); j++) {
                if (!cur1.get(j).equals(cur2.get(j))) {
                    return false;
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        int maxValue = 10;
        int maxLen = 100;
        int maxK = 6;
        int testTimes = 100000;
        System.out.println("测试开始");
        for (int i = 0; i < testTimes; i++) {
            Data testData = randomData(maxValue, maxLen);
            int k = (int) (Math.random() * maxK) + 1;
            int[] arr = testData.arr;
            boolean[] op = testData.op;
            List<List<Integer>> ans1 = topK(arr, op, k);
            List<List<Integer>> ans2 = compare(arr, op, k);
            if (!sameAnswer(ans1, ans2)) {
                for (int j = 0; j < arr.length; j++) {
                    System.out.println(arr[j] + " , " + op[j]);
                }
                System.out.println(k);
                System.out.println(ans1);
                System.out.println(ans2);
                System.out.println("出错了!");
                break;
            }
        }
        System.out.println("测试结束");
    }

}