堆 — — 手动改写堆及经典面试题
1 改写堆
在Java中系统已经提供了PriorityQueue优先级队列来作为堆,但是在实际业务场景中我们可能需要一些额外的功能
比如:
1)已经入堆的元素,如果参与排序的指标发生变化,系统提供的堆调整复杂度太高【O(N)】,因为不记得每个元素所在的位置,无法实现O(logN)级别
2)系统提供的堆只能弹出堆顶,无法做到自由删除任何一个堆中元素,或者说无法在时间复杂度为O(logN)内完成
根本原因:无反向索引表【给元素,返回元素位置】
关键步骤:
1)建立反向索引表
2)建立比较器
3)具体逻辑实现【各种结构相互配合】
1.1 基本属性
private ArrayList<T> heap;//存储元素
private int heapSize;//堆元素个数
private HashMap<T, Integer> indexMap;//反向索引表,根据元素val查找位置
private Comparator<? super T> comp;//比较器
1.2 主要增强点【移除指定元素】
//移除指定元素
//用末尾元素替换obj元素
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);
}
}
1.3 堆重构【resign:上浮、下沉】
public void resign(T obj) {
//只会走一个
heapInsert(indexMap.get(obj));
heapify(indexMap.get(obj));
}
//返回堆中所有元素
public List<T> getAllElements() {
List<T> res = new ArrayList<>();
for (T t : heap) {
res.add(t);
}
return res;
}
//上浮
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(index, best);
index = best;
left = index * 2 + 1;//继续下沉
}
}
1.4 全部代码
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
/**
* 改写堆:必须是非基础类型(不能是数字等),有基础类型的话必须外包一层
* 原因:反向索引表基于map实现,如果是基础类型,不能添加重复元素【会被覆盖】
*/
public class HeapGreater<T> {
private ArrayList<T> heap;//存储元素
private int heapSize;//堆元素个数
private HashMap<T, Integer> indexMap;//反向索引表,根据元素val查找位置
private Comparator<? super T> comp;//比较器
public HeapGreater(Comparator<? super T> c) {
heap = new ArrayList<>();
indexMap = new HashMap<>();
heapSize = 0;
this.comp = c;
}
public boolean isEmpty() {
return heapSize == 0;
}
public int size() {
return heapSize;
}
public boolean contains(T val) {
return indexMap.containsKey(val);
}
//返回堆顶元素
public T peek() {
return heap.get(0);
}
public void push(T val) {
heap.add(val);
indexMap.put(val, heapSize);//存入新元素位置信息
//添加新元素后,看元素是否上浮
heapInsert(heapSize++);
}
public T pop() {
//堆:弹出堆顶元素
T ans = heap.get(0);
//0位置上的数与末位相换
swap(0, heapSize - 1);
indexMap.remove(ans);//反向索引表移除该元素key
heap.remove(--heapSize);
//末位换过来的元素要下沉
heapify(0);
return ans;
}
//移除指定元素
//用末尾元素替换obj元素
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> res = new ArrayList<>();
for (T t : heap) {
res.add(t);
}
return res;
}
//上浮
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(index, best);
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);
}
}
2 经典面试题【业务场景:topK问题,电商中奖问题】
2.1 题目介绍
给定一个整型数组,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]和op[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,如果候选区购买数最多的用户,已经足以进入得奖区,
该用户就会替换得奖区中购买数最少的用户(大于才能替换),
如果得奖区中购买数最少的用户有多个,就替换最早进入得奖区的用户
如果候选区中购买数最多的用户有多个,机会会给最早进入候选区的用户 - 8,候选区和得奖区是两套时间,
因用户只会在其中一个区域,所以只会有一个区域的时间,另一个没有
从得奖区出来进入候选区的用户,得奖区时间删除,
进入候选区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i)
从候选区出来进入得奖区的用户,候选区时间删除,
进入得奖区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i) - 9,如果某用户购买数==0,不管在哪个区域都离开,区域时间删除,
离开是指彻底离开,哪个区域也不会找到该用户
如果下次该用户又发生购买行为,产生>0的购买数,
会再次根据之前规则回到某个区域中,进入区域的时间重记
问题:
请遍历arr数组和op数组,遍历每一步输出一个得奖名单
public List<List> topK (int[] arr, boolean[] op, int k)
2.2 思路
利用增强堆进行解答,理解其中业务逻辑
2.3 代码
public class TopKQuestion{
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
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) {
candHeap.remove(c);
} else {
candHeap.resign(c);
}
} else {
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);
}
}
}
}
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;
}
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);
}
}