电商需求-求用户购买商品TopK问题

314 阅读7分钟

「这是我参与2022首次更文挑战的第31天,活动详情查看:2022首次更文挑战」。

一、需求

  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用户退货了一件商品…

  2. 一对arr[i]和op[i]就代表一个事件: 用户号为arr[i],op[i] == T就代表这个用户购买了一件商品 op[i] == F就代表这个用户退货了一件商品 现在你作为电商平台负责人,你想在每一个事件到来的时候, 都给购买次数最多的前K名用户颁奖。 所以每个事件发生后,你都需要一个得奖名单(得奖区)。

  3. 得奖系统的规则:

    1. 如果某个用户购买商品数为0,但是又发生了退货事件,则认为该事件无效,得奖名单和上一个事件发生后一致,例子中的5用户
    2. 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的购买数,会再次根据之前规则回到某个区域中,进入区域的时间重记
  4. 请遍历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;
}