「这是我参与2022首次更文挑战的第31天,活动详情查看:2022首次更文挑战」。
一、需求
给定一个整型数组,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名用户颁奖。 所以每个事件发生后,你都需要一个得奖名单(得奖区)。
得奖系统的规则:
- 如果某个用户购买商品数为0,但是又发生了退货事件,则认为该事件无效,得奖名单和上一个事件发生后一致,例子中的5用户
- 2 某用户发生购买商品事件,购买商品数+1,发生退货事件,购买商品数-1
- 每次都是最多K个用户得奖,K也为传入的参数,如果根据全部规则,得奖人数确实不够K个,那就以不够的情况输出结果
- 得奖系统分为得奖区和候选区,任何用户只要购买数>0, 一定在这两个区域中的一个
- 购买数最大的前K名用户进入得奖区,在最初时如果得奖区没有到达K个用户,那么新来的用户直接进入得奖区
- 如果购买数不足以进入得奖区的用户,进入候选区
- 如果候选区购买数最多的用户,已经足以进入得奖区,该用户就会替换得奖区中购买数最少的用户(大于才能替换),如果得奖区中购买数最少的用户有多个,就替换最早进入得奖区的用户,如果候选区中购买数最多的用户有多个,机会会给最早进入候选区的用户
- 候选区和得奖区是两套时间, 因用户只会在其中一个区域,所以只会有一个区域的时间,另一个没有从得奖区出来进入候选区的用户,得奖区时间删除,进入候选区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i),从候选区出来进入得奖区的用户,候选区时间删除,进入得奖区的时间就是当前事件的时间(可以理解为arr[i]和op[i]中的i)
- 如果某用户购买数==0,不管在哪个区域都离开,区域时间删除,离开是指彻底离开,哪个区域也不会找到该用户,如果下次该用户又发生购买行为,产生>0的购买数,会再次根据之前规则回到某个区域中,进入区域的时间重记
请遍历arr数组和op数组,遍历每一步输出一个得奖名单
public List<List> topK (int[] arr, boolean[] op, int k)
二、分析
一个得奖区和一个候选区,(top2)得奖区大小为2,候选区空着
5号用户买了1件商品,此时时间点是0,得奖区(5,1,0)
3号用户买了1件商品,此时时间点是1,得奖区(3,1,1)
此时TOP2为5号用户和3号用户
1号用户买了1件商品,此时得奖区满了,所以1号用户放候选区,候选区(1,1,2),它不能进入得奖区,因为它的购买数为1,候选区的用户的购买数只有大于得奖区的用户才能替换(进入),相等维持老的用户的获奖名单(得奖区)
1号用户又买了1件商品,所以1号用户在候选区的商品更新为2,候选区(1,2,2),时间点不更新,还是最初进来的时候,现在候选区1号用户能进入的得奖区,替换得奖区购买商品数量少的用户,如果得奖区存在相同购买商品的用户,则替换比较早进入得奖区的用户(5号用户),意思就是我新进来的人我一直买,你老的人一直没买,还占位置,给我出去,候选区1号用户(1,2,2)从候选区抽取,得奖区5号用户(5,1,0)从得奖区出去,候选区1号用户进入的得奖区(1,2,3),时间点为3,5号用户待命进去候选区,候选区(5,1,3)
此时TOP2位1号用户和3号用户
3号用户退了1件货,得奖区(3,0,1),则从得奖区中删掉,候选区也不留它
5号用户又进入了得奖区,得奖区(5,1,4)
此时TOP2位1号用户和5号用户
如果候选区中存在一些用户的购买货物数量相等,在得奖区不满且购买数小于候选区的用户,则早进入候选区的用户进入得奖区(照顾老客户)
时间复杂度估算:每个事件到来的时候,得奖区logk,候选区logN,生成k个链(topk),总共N的事件,所以整体的时间复杂度为:O(N * (logN + logK + K))
候选区大根堆
得奖区小根堆
候选区大根堆的堆顶用户与得奖区小根堆的堆顶用户交换
三、实现
// 用户购买行为定义
public static class Customer {
public int id; // i号用户
public int buy; // i号用户购买数量
public int enterTime; // i号用户事件时间
public Customer(int v, int b, int o) {
id = v;
buy = b;
enterTime = o;
}
}
// 候选区比较器:购买数量从大到小排序,如果购买数量相等,则按用户进入时间从小到大排序(早->晚)
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 List<List<Integer>> compare(int[] arr, boolean[] op, int k) {
HashMap<Integer, Customer> map = new HashMap<>(); // key:几号用户
ArrayList<Customer> cands = new ArrayList<>(); // 候选区集合
ArrayList<Customer> daddy = new ArrayList<>(); // 得奖区集合
List<List<Integer>> ans = new ArrayList<>(); // 每个时间点的TopK
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);
}
// c
// 下面做
if (!cands.contains(c) && !daddy.contains(c)) {
c.enterTime = i;
if (daddy.size() < k) {
daddy.add(c);
} else {
cands.add(c);
}
}
cleanZeroBuy(cands);
cleanZeroBuy(daddy);
cands.sort(new CandidateComparator());
daddy.sort(new DaddyComparator());
move(cands, daddy, k, i);
ans.add(getCurAns(daddy));
}
return ans;
}
private 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);
}
}
}
private 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 class WhosYourDaddy {
private HashMap<Integer, Customer> customers;
private HeapGreater<Customer> candHeap; // 候选区大根堆
private HeapGreater<Customer> daddyHeap; // 得奖区小根堆
private final int daddyLimit; // TopK
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;
}