前两篇文章我们介绍了有关 堆排序、大小根堆 以及 手写加强堆 的相关知识,(还没看过上篇文章的赶快 点我 查看哦!)
本篇文章我们使用 加强堆 完成一道较有难度的 TopK 问题!
假设现在商场中顾客会进行购买或退货两种操作,每次操作只能购买或退货一件商品。给定两个等长的整形数组 arr
和布尔型数组 op
,arr[i]
表示顾客的编号,op[i]
表示顾客的操作,T 代表该顾客购买了一件商品,F 代表该顾客退了一件商品。例如:
arr = [3, 3, 1, 2, 1, 2, 5…]
op = [T, T, T, T, F, T, F…]
表示 :
3 用户购买一件商品,3 用户购买一件商品,
1 用户购买一件商品,2 用户购买一件商品,
1 用户退货一件商品,2 用户购买一件商品,
5 用户退货一件商品…
现在你作为电商平台负责人,想在每一个事件到来的时候(某个用户购买或者退货的时候),都给购买次数最多的前 K 名用户颁奖。所以 每个事件发生后 ,你 都需要得到 得奖名单 。
题目规则有些多,慢慢理解清楚!(其实就是最生活化的排名问题哦~)
首先分析结构体的定义:每个顾客有一个用于区分身份的 id
、购买商品数量的 buy
,根据题目描述,每个顾客仅有一个进入 得奖区 或 候选区 的时间 enterTime
。
public static class Customer {
public int id;
public int buy;
public int enterTime;
public Customer(int id, int buy, int enterTime) {
this.id = id;
this.buy = buy;
this.enterTime = 0;
}
}
接下来我们定义比较器,思考两个候选区中顾客的比较方式:
候选区
:在候选区的顾客若购买商品数量足以达到进入得奖区的要求(超过了得奖区的最后一名),则弹出候选区,进入得奖区。因此,候选区应构建一个大根堆,每次堆顶弹出的是购买数量最多的顾客。
得奖区
:若候选区有顾客来了,则弹出候选区,进入得奖区。因此,得奖区应构建一个小根堆,让得奖区中购买数最小的被替换出去。
注意:这里的堆为加强堆(上篇文章 里介绍的哦)。
public static class CandidateComparator implements Comparator<Customer> {
@Override
public int compare(Customer o1, Customer o2) {
// 大根堆,这样才能从 候选堆里 选出买的最多的进入 TopK
// 购买数一样,先让 时间小 的进入(等待的时间长)
return o1.buy != o2.buy ? (o2.buy - o1.buy) : (o1.enterTime - o2.enterTime);
}
}
public static class TopComparator implements Comparator<Customer> {
@Override
public int compare(Customer o1, Customer o2) {
// 小根堆,这样才能弹出(淘汰)购买数最小的
// 购买数一样,先让 时间小 的出去(在TopK中待的时间已经很长了,就走吧)
return o1.buy != o2.buy ? (o1.buy - o2.buy) : (o1.enterTime - o2.enterTime);
}
}
接下来分析函数主要功能部分。
使用 哈希表 实现查找用户 id 与 购买商品数量 的索引表功能。
当某个顾客到来后 (即事件发生时),我们按照规则分析情况:
public static class topK {
// 反向索引表
private HashMap<Integer, Customer> customers;
// 候选堆
private HeapGreater<Customer> candHeap;
// TopK 堆
private HeapGreater<Customer> topHeap;
private final int K;
public topK(int k) {
customers = new HashMap<Integer, Customer>();
candHeap = new HeapGreater<>(new CandidateComparator());
topHeap = new HeapGreater<>(new TopComparator());
K = k;
}
// 处理 i 号事件
public void operate(int time, int id, boolean buyOrRefund) {
// 未买就退
if (!buyOrRefund && !customers.containsKey(id)) {
return;
}
// 买了,但没 id ,说明这是个新人,new 一个
if (!customers.containsKey(id)) {
customers.put(id, new Customer(id, 0, 0));
}
// 有id,老顾客
Customer c = customers.get(id);
if (buyOrRefund) {
c.buy++;
} else {
c.buy--;
}
if (c.buy == 0) {
customers.remove(id);
}
if (!candHeap.contains(c) && !topHeap.contains(c)) {
// 新人均不在这两个堆里
// 得奖区未满,进得奖区
if (topHeap.size() < K) {
c.enterTime = time;
topHeap.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 {
// 在 topK 堆里
if (c.buy == 0) {
topHeap.remove(c);
} else {
// 重新调整堆
topHeap.resign(c);
}
}
// 一个顾客判断结束后,开始比较两个堆大小
kMove(time);
}
private void kMove(int time) {
if (candHeap.isEmpty()) {
return;
}
// 得奖区未满,直接进
if (topHeap.size() < K) {
Customer p = candHeap.pop();
p.enterTime = time;
topHeap.push(p);
} else {
// 候选区的头 能够替换 得奖区的尾
if (candHeap.peek().buy > topHeap.peek().buy) {
Customer oldK = topHeap.pop();
Customer newK = candHeap.pop();
oldK.enterTime = time;
newK.enterTime = time;
candHeap.push(oldK);
topHeap.push(newK);
}
}
}
// 输出 得奖区 所有的顾客
public List<Integer> getAllK() {
List<Customer> customers = topHeap.getAllElements();
List<Integer> ans = new ArrayList<>();
for (Customer c : customers) {
ans.add(c.id);
}
return ans;
}
}
注意看代码上面的注释哦!
你学会了么?
~点赞 ~ 关注 ~ 星标 ~ 不迷路 ~ !!!