一、比较器
任何比较器: compare方法里,遵循一个统一的规范: 返回负数的时候,认为第一个参数应该排在前面 返回正数的时候,认为第二个参数应该排在前面 返回0的时候,认为无所谓谁放前面
即:
1、< 0 时,第一个参数小于第二个参数
2、= 0 时,第一个参数等于第二个参数
3、> 0 时,第一个参数大于第二个参数
二、堆
1、完全二叉树:一个满的二叉树或从左到右依次变满的二叉树
2、数组来表示完全二叉树,任何一个 i 位置结点有以下规律
1)左孩子 = 2 * i + 1
2)右孩子 = 2 * i + 2
3)父节点 = (i - 1) / 2
3、堆:数组 + size
1)大根堆:每一颗子树的头结点都大于其孩子结点
2)小根堆:每一颗子树的头结点都小于其孩子结点
heapInsert:新加进入的数
新数据停在了 index 位置,依次往上看父节点
如果比父节点大则进行交换,否则不大于或达到了0位置了,不再继续
heapIfy:删掉头结点(0位置的数)
拿最后一个位置的数放在 0 位置,堆 size-1
此时很可能不是堆结构,需要调整堆结构
0 位置看左右孩子,找到较大孩子,看一下能不能大于自己
如果大于则2个位置进行交换,依次进行,直到干不掉了或堆最后了停
heapInsert 与 heapIfy 二者只会发生一个,在不确定如何调整堆的情况下,二者依次调用可保万无一失(加强堆的使用)
4、堆的类别:大根堆与小根堆
编码:
heapInsert -> 依次加入数字
// 新加进来的数,现在停在了index位置,请依次往上移动, // 移动到0位置,或者干不掉自己的父亲了,停! private void heapInsert(int[] arr, int index) { // [index] 父节点 [index-1]/2 // 头结点 index == 0 while (arr[index] > arr[(index - 1) / 2]) { swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } }
heapIf -> 从 index 位置往下看
// 从index位置,往下看,不断的下沉 // 停:较大的孩子都不再比index位置的数大;已经没孩子了 private void heapify(int[] arr, int index, int heapSize) { // 左孩子下标 int left = index * 2 + 1; // 还有孩子则继续 while (left < heapSize) { // 如果有左孩子,有没有右孩子,可能有可能没有! // 把较大孩子的下标,给largest int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; largest = arr[largest] > arr[index] ? largest : index; // 干不掉父节点了 if (largest == index) { break; } // index和较大孩子,要互换 swap(arr, largest, index); index = largest; left = index * 2 + 1; } }
大根堆
public class MyMaxHeap { private int[] heap; private final int limit; private int heapSize; public MyMaxHeap(int limit) { heap = new int[limit]; this.limit = limit; heapSize = 0; } public boolean isEmpty() { return heapSize == 0; } public boolean isFull() { return heapSize == limit; } public void push(int value) { if (heapSize == limit) { throw new RuntimeException("heap is full"); } heap[heapSize] = value; // value heapSize heapInsert(heap, heapSize++); } // 用户此时,让你返回最大值,并且在大根堆中,把最大值删掉 // 剩下的数,依然保持大根堆组织 public int pop() { int ans = heap[0]; swap(heap, 0, --heapSize); heapify(heap, 0, heapSize); return ans; } // 新加进来的数,现在停在了index位置,请依次往上移动, // 移动到0位置,或者干不掉自己的父亲了,停! private void heapInsert(int[] arr, int index) { // [index] [index-1]/2 // index == 0 while (arr[index] > arr[(index - 1) / 2]) { swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } // 从index位置,往下看,不断的下沉 // 停:较大的孩子都不再比index位置的数大;已经没孩子了 private void heapify(int[] arr, int index, int heapSize) { // 左孩子下标 int left = index * 2 + 1; // 还有孩子则继续 while (left < heapSize) { // 如果有左孩子,有没有右孩子,可能有可能没有! // 把较大孩子的下标,给largest int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; largest = arr[largest] > arr[index] ? largest : index; // 干不掉父节点了 if (largest == index) { break; } // index和较大孩子,要互换 swap(arr, largest, index); index = largest; left = index * 2 + 1; } } private void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } }
堆调整复杂度:log(N)
三、堆排序
思路:
1)将数组调整为大根堆 2)将数组 0 位置的元素与 N-1 位置的元素进行交换 3)heapSize-- 4)数组 0 位置的元素 heapIfy 调整堆结构,继续保持大根堆 5)重新将 0 位置的元素与 N-2 位置的元素进行交换,周而复始,直到 heapSize = 1
编码:
public void heapSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 建立大根堆方式 1 一个一个往堆上放,从上往下建堆 // O(N*logN) // for (int i = 0; i < arr.length; i++) { // O(N) // heapInsert(arr, i); // O(logN) // } // 建立大根堆方式 2 从下往上建堆 // O(N) for (int i = arr.length - 1; i >= 0; i--) { heapify(arr, i, arr.length); } // heapSize int heapSize = arr.length; // 0 位置与最后一个元素进行交换,heapSize-1 swap(arr, 0, --heapSize); // O(N*logN) // 只要 heapSize 不为1,继续周而复始 while (heapSize > 1) { // O(N) // 调整大根堆 heapify(arr, 0, heapSize); // O(logN) swap(arr, 0, --heapSize); // O(1) } } // arr[index]刚来的数,往上 public void heapInsert(int[] arr, int index) { while (arr[index] > arr[(index - 1) / 2]) { swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } // arr[index]位置的数,能否往下移动 public void heapify(int[] arr, int index, int heapSize) { int left = index * 2 + 1; // 左孩子的下标 while (left < heapSize) { // 下方还有孩子的时候 // 两个孩子中,谁的值大,把下标给largest // 1)只有左孩子,left -> largest // 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest // 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; // 父和较大的孩子之间,谁的值大,把下标给largest largest = arr[largest] > arr[index] ? largest : index; if (largest == index) { break; } swap(arr, largest, index); index = largest; left = index * 2 + 1; } } public void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
堆排序复杂度:N * log(N)
四、堆排序应用
1、给定一个数组,已知排好序元素移动的距离不超过K
思路:
1)建立长度为K的小根堆 2)弹出小根堆的堆顶元素放在 0 位置 3)加入 K + 1 的元素,再弹出小根堆的堆顶元素,放在 1 位置,周而复始
编码:
public void sortedArrDistanceLessK(int[] arr, int k) { if (k == 0) { return; } // 默认小根堆 PriorityQueue<Integer> heap = new PriorityQueue<>(); int index = 0; // 0...K-1 // 建立长度为K的小根堆 for (; index <= Math.min(arr.length - 1, k - 1); index++) { heap.add(arr[index]); } int i = 0; // 加入一个元素,弹出堆顶元素 for (; index < arr.length; i++, index++) { heap.add(arr[index]); arr[i] = heap.poll(); } // 最后弹出所有堆中的元素 while (!heap.isEmpty()) { arr[i++] = heap.poll(); } }
2、最大线段重合问题
给定很多线段,每个线段都有两个数[start,end] 表示线段开始位置和结束位置,左右都是闭区间 规定:线段的开始和结束位置一定都是整数值 线段重合区域的长度必须>=1 返回线段最多重合区域中,包含了几条线段 解题方法: 1)统计找每个小数位的区间有多少线段,求最大值得出结果 2)利用小根堆,根据线段开始的值进行排序,将线段的结束值依次加入到小根堆 堆顶元素小于等于当前结束值时弹出,弹出后的堆的大小即是包含的线段数 取最大值即可得到结果 思路:每条线段开始的点,有多少个能够进行穿过,找出最多的
编码:
// 统计法 public 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; // 找到最小值到最大值范围中的每个固定小数位的覆盖的个数 for (double p = min + 0.5; p < max; p += 1) { int cur = 0; // 看每个线段是否包含了 p 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; } // 小根堆法 public 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 = Math.max(max, heap.size()); } return max; } // 线段类 public class Line { public int start; public int end; public Line(int s, int e) { start = s; end = e; } } // 比较器 public class StartComparator implements Comparator<Line> { @Override public int compare(Line o1, Line o2) { return o1.start - o2.start; } } // 与方法2类似 public int maxCover3(int[][] m) { Arrays.sort(m, (a, b) -> (a[0] - b[0])); // 准备好小根堆,和课堂的说法一样 PriorityQueue<Integer> heap = new PriorityQueue<>(); int max = 0; for (int[] line : m) { while (!heap.isEmpty() && heap.peek() <= line[0]) { heap.poll(); } heap.add(line[1]); max = Math.max(max, heap.size()); } return max; }
五、加强堆
思路:
1)堆结构 2)反向索引表 3)堆大小 4)比较器 堆调整时,反向索引表同步调整
编码:
public class HeapGreater<T> { private ArrayList<T> heap; // 堆结构 private HashMap<T, Integer> indexMap; // 反向索引表 private int heapSize; // 堆大小 private Comparator<? super T> comp; // 比较器 public HeapGreater(Comparator<T> c) { heap = new ArrayList<>(); indexMap = new HashMap<>(); heapSize = 0; comp = c; } 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); // 堆顶的 0 位置元素重新调整堆结构 heapify(0); return ans; } // 删除堆中元素 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; } } // 交换元素,同步更新索引表 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、topK 问题(抽奖)
业务要求:
给定一个整型数组,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用户退货了一件商品… 得奖系统的规则: 1,如果某个用户购买商品数为0,但是又发生了退货事件, 则认为该事件无效,得奖名单和上一个事件发生后一致,例子中的5用户 2,某用户发生购买商品事件,购买商品数+1,发生退货事件,购买商品数-1 3,每次都是最多K个用户得奖,K也为传入的参数 如果根据全部规则,得奖人数确实不够K个,那就以不够的情况输出结果 4,得奖系统分为得奖区和候选区,任何用户只要购买数>0, 一定在这两个区域中的一个 5,购买数最大的前K名用户进入得奖区, 在最初时如果得奖区没有到达K个用户,那么新来的用户直接进入得奖区 6,如果购买数不足以进入得奖区的用户,进入候选区 7,如果候选区购买数最多的用户,已经足以进入得奖区, 该用户就会替换得奖区中购买数最少的用户(大于才能替换), 如果得奖区中购买数最少的用户有多个,就替换最早进入得奖区的用户 如果候选区中购买数最多的用户有多个,机会会给最早进入候选区的用户 8,候选区和得奖区是两套时间, 因用户只会在其中一个区域,所以只会有一个区域的时间,另一个没有 从得奖区出来进入候选区的用户,得奖区时间删除, 进入候选区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i) 从候选区出来进入得奖区的用户,候选区时间删除, 进入得奖区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i) 9,如果某用户购买数==0,不管在哪个区域都离开,区域时间删除, 离开是指彻底离开,哪个区域也不会找到该用户 如果下次该用户又发生购买行为,产生>0的购买数, 会再次根据之前规则回到某个区域中,进入区域的时间重记 请遍历arr数组和op数组,遍历每一步输出一个得奖名单
编码:两个堆倒数据:候选区大根堆,得奖区小根堆,候选区堆顶看是否能替换得奖区堆顶
- 暴力解
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<>(); for (int i = 0; i < arr.length; i++) { int id = arr[i]; boolean buyOrRefund = op[i]; // 没买货且退货,忽略此事件,得奖的还是原来得奖区的元素 if (!buyOrRefund && !map.containsKey(id)) { ans.add(getCurAns(daddy)); continue; } // 没有发生:用户购买数为0并且又退货了 // 用户之前购买数是0,此时买货事件 // 用户之前购买数>0, 此时买货 // 用户之前购买数>0, 此时退货 if (!map.containsKey(id)) { map.put(id, new Customer(id, 0, 0)); // 后面处理 } // 买、卖 Customer c = map.get(id); if (buyOrRefund) { c.buy++; } else { c.buy--; } if (c.buy == 0) { map.remove(id); } // 新用户 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()); // 得奖区排序:购买数小的排前面,一样时,先进来的放前面 daddy.sort(new DaddyComparator()); // 看候选区是否能替换得奖区的记录 move(cands, daddy, k, i); // 得到答案 ans.add(getCurAns(daddy)); } return ans; } 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 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 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 WhosYourDaddy { private HashMap<Integer, Customer> customers; // 顾客信息,key = id, value = info 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 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)) { // 看一下是否买卖数为0了,是则移除,否则重新调整候选区堆结构 if (c.buy == 0) { candHeap.remove(c); } else { candHeap.resign(c); } // 在抽奖区了 } else { // 看一下是否买卖数为0了,是则移除,否则重新调整抽奖区堆结构 if (c.buy == 0) { daddyHeap.remove(c); } else { daddyHeap.resign(c); } } // 候选区和抽奖区顾客的移动处理 daddyMove(time); } // 得到所有抽奖区的顾客 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); } } } } // 主方法,得到抽奖区的前K个记录 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; }