堆 — — 手动改写堆及经典面试题【TopK】

堆 — — 手动改写堆及经典面试题

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);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值