0
点赞
收藏
分享

微信扫一扫

Java 集合扩展 | 索引堆实现

程序员阿狸 2022-03-20 阅读 34

1、前言

好像Java自带的集合体中并没有原生索引堆实现, 甚至原生的普通堆都没有,而优先级队列PriorityQueue虽然可以当作堆来使用,虽然底层是用了堆的思想实现, 但是它本质还是属于队列体系。

2、什么是堆

  • 特点1: 本质是一棵完全二叉树
  • 特点2: 每个节点的值比它左右孩子节点的值都小 或者 都大
    • 比孩子节点值都大的叫 最大堆, 反之叫最小堆

如下图两颗完全二叉树就是属于堆

3、堆的应用场景

堆可以以O(1)时间复杂度快速的获得集合里的最大 或者 最小值。

应用场景

  • 生成优先级队列
  • 大数据量的计算TopN问题
  • 计算动态数据集合的中位数、百分位数
  • 高性能定时任务执行器

4、堆实现原理

4.1 入堆

4.2 出堆

如下图的将根节点16先与尾节点4交换,然后再删除尾节点。

4.3 修复

修复就是当堆内的节点之间的关系不再满足于 每个节点的值比它左右孩子节点的值都小 或者 都大的特点时进行的一次修复操作。 主要分为上浮修复下沉修复

4.3.1 上浮修复

上浮修复的实现就是从某一个节点开始不断与父节点进行比较交换, 假设是最大堆,如果当前节点比父节点大, 则此节点应该往上浮, 所以交换它们两个值。 依次类推, 父节点继续与爷爷节点比较交换…, 直到满足条件 或者 到达 根部。

4.3.2 下沉修复

下浮修复的实现就是从某一个节点开始不断与子节点进行比较交换, 假设是最大堆,如果当前节点比子节点小, 则此节点应该往下浮, 所以交换它们两个值。 依次类推, 子节点继续与孙子节点比较交换…, 直到满足条件 或者 到达 尾部。

下面是一个最大堆的 依次出堆顶元素的操作, 并从堆顶节点开始进行下沉修复操作的过程
请添加图片描述

5、Java如何使用堆

  • 使用优先级队列PriorityQueue替代堆的使用, 每次出队可以把优先级最高的元素出队。 元素的优先级默认按照添加的元素的自然顺序判断, 也可以创建构造函数的时候指定自定义比较器。

  • PriorityQueue是无界队列, 如果是要只保留N个元素, 需要我们自己控制堆的容量

5.1 堆排序

        // 优先级队列可当 堆使用, 默认不指定时最小堆
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        //PriorityQueue<Integer> minHeap = new PriorityQueue<>((a,b)-> b-a); //最大堆
		
		// 往堆添加元素
        int[] arr = {3,5,1,2,8,4};
        for (int i = 0; i < arr.length; i++) {
            minHeap.add(arr[i]);
        }
        
        // 此时的堆结构为
        /*
                 1
              2    3
            5 8   4
         */

        // 依次出堆顶, 输出为:  1,2,3,4,5,8
        while (!minHeap.isEmpty()){
            Integer poll = minHeap.poll();
            System.out.println(poll);
        }

5.1 求解TopN问题

求最小的K个数,可以使用最大堆, 因为每次出堆会把最大的出掉,所以堆剩下的就是最小的。
求最大的K个数,可以使用最小堆,因为每次出堆会把最小的出掉, 所以堆剩下的就是最大的。

测试使用最大堆求最小的K个数

         // 创建最大堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a,b) -> b-a);

        //往数组添加10万个数
        ArrayList<Integer> arrList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            arrList.add(i);
        }

        // 将数组数据顺序打乱
        Collections.shuffle(arrList);

        // 设置求最小的K个数
        int K = 10;

        // 往堆添加元素
        for (Integer value : arrList) {
            if (maxHeap.size() < K){
                maxHeap.add(value);
            }else if (value < maxHeap.peek()){
                // 找到更小的数value,则把它添加到堆
                maxHeap.remove();
                maxHeap.add(value);
            }
        }

        // 依次出堆顶, 取出最小的K个数
        while (!maxHeap.isEmpty()){
            Integer poll = maxHeap.poll();
            System.out.println(poll);
        }

测试使用最小堆求最大的K个数

         // 创建最大堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>();

        //往数组添加10万个数
        ArrayList<Integer> arrList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            arrList.add(i);
        }

        // 将数组数据顺序打乱
        Collections.shuffle(arrList);

        // 设置求最小的K个数
        int K = 10;

        // 往堆添加元素
        for (Integer value : arrList) {
            if (maxHeap.size() < K){
                maxHeap.add(value);
            }else if (value > maxHeap.peek()){
                // 找到更大的数value,则把它添加到堆
                maxHeap.remove();
                maxHeap.add(value);
            }
        }

        // 依次出堆顶, 取出最小的K个数
        while (!maxHeap.isEmpty()){
            Integer poll = maxHeap.poll();
            System.out.println(poll);
        }

6、索引堆

  • 索引堆其实也是堆
  • 将数据和索引这两部分分开存储,与普通堆不同此时的每个节点存放一个索引,而这个索引指向真正的对象值。但是构建索引堆还是依赖于索引对应的对象的实际值去比较和交换

索引堆结构如下图所示

  • 完全二叉树的每个节点存放的是索引Index的值, 而data里面维护了Index 和 实际值的映射关系, 然后比较交换的时候是根据data的实际值去构建堆
  • 其实和普通堆实现起来只是加多了一层映射关系,核心实现几乎一样。
    在这里插入图片描述

与普通堆对比

  • 普通堆在交换操作可能耗费时间较大,索引堆优化了交换元素的消耗。
  • 普通堆建成后很难索引到它,很难去改变它,但索引堆却可以以0(1)级别索引到具体元素, 也就是说它可以支持很方便的修改堆元素的操作
  • 具有O(1)级别按索引访问的特点又有堆快速取出极值的特点。
  • 甚至可以认为是具有堆功能的Map, 不过其中堆的构建是根据索引对应的值 而不是索引。

6.1、索引堆实现代码

/** 索引堆
 * @author burukeyou
 * @param <KEY>     索引
 * @param <VALUE>   索引对应的值      
 */
public class IndexHeap<KEY, VALUE extends Comparable<VALUE>>  {

    // 实际的索引树    keyTree[下标i] = 索引
    private ArrayList<KEY> keyTree;
    
    // Map<索引,下标>
    private Map<KEY,Integer> keyIndexMap;

    // Map<索引,元素>
    private Map<KEY, VALUE>  keyDataMap;
    
    // 是否最小堆
    private boolean isMinHeap;

    // 堆的最大容量
    private Integer maxSize;

    public IndexHeap(boolean isMinHeap) {
        this.isMinHeap = isMinHeap;
        this.keyTree = new ArrayList<>();
        this.keyIndexMap = new HashMap<>();
        this.keyDataMap =new HashMap<>();
    }

    public IndexHeap(boolean isMinHeap, Integer maxSize) {
        this(isMinHeap);
        this.maxSize = maxSize;
    }

    public int size() {
        return keyTree.size();
    }

    public boolean isEmpty() {
        return keyTree.isEmpty();
    }

    public VALUE get(KEY key) {
        return keyDataMap.get(key);
    }

    public boolean containsKey(KEY key) {
        return keyDataMap.containsKey(key);
    }

    /**
     * 替换索引的值
     * @param key
     * @param e
     */
    public void replace(KEY key, VALUE e) {
        if (!keyDataMap.containsKey(key)){
            return;
        }
        keyDataMap.put(key, e);
        // 获取索引对应的节点
        int j = keyIndexMap.get(key);
        // 从节点j开始修复堆
        moveUp(j);
        moveDown(j);
    }

    /**
     * 查看堆顶元素
     */
    public Entry<KEY,VALUE> peek() {
        KEY key = keyTree.get(0);
        VALUE value = keyDataMap.get(key);
        return new Entry<>(key,value);
    }

    /**
     * 堆顶元素出堆
     */
    public Entry<KEY,VALUE> remove() {
        if (isEmpty()){
            return null;
        }

        Entry<KEY,VALUE> ret = peek();

        //1.把尾节点与堆头交换存放的索引值
        swap(keyTree,0,size()-1);

        // 更新
        keyIndexMap.put(keyTree.get(0),0);
        KEY lastKey = keyTree.get(size() - 1);
        keyIndexMap.remove(lastKey);
        keyDataMap.remove(lastKey);
        keyTree.remove(size() - 1);

        // 从堆顶节点开始下浮修复
        moveDown(0);
        return ret;
    }

    /**
     * 入堆 (索引KEY不存在时)
     * @param key       索引
     * @param e         实际值
     */
    public void add(KEY key, VALUE e) {
        if (!keyDataMap.containsKey(key)) {
            keyTree.add(key);
            keyDataMap.put(key, e);
            keyIndexMap.put(key, size() - 1);
            moveUp(size() - 1);
        }
    }

    /**
     * 入堆 (索引KEY存在时进行替换)
     * @param key       索引
     * @param e         实际值
     */
    public void addOrReplace(KEY key, VALUE e){
        if (!keyDataMap.containsKey(key)){
            add(key,e);
        }else{
            replace(key,e);
        }
    }

    /**
     *  从i开始向上浮动修复堆
     * @param i        索引树的节点下标
     */
    private void moveUp(int i) {
        while (i > 0){
            VALUE iValue = getValueByIndex(i);
            VALUE parentValue = getValueByIndex(parent(i));

            // 最小堆
            if (isMinHeap && iValue.compareTo(parentValue) > 0){
                break;
            }

            // 最大堆
            if (!isMinHeap && iValue.compareTo(parentValue) < 0){
                break;
            }

            int patentIndex = parent(i);
            swapAndRestKeyIndexMap(keyTree,i,patentIndex);
            i = parent(i);
        }
    }

    public VALUE getValueByIndex(int index){
        return keyDataMap.get(keyTree.get(index));
    }

    /**
     *  从i开始向下浮动修复堆
     * @param i          索引树的节点下标
     */
    private void moveDown(int i) {
        //是否有孩子节点,因为是完全二叉树,只判断左孩子即可
        while(leftChildren(i) < size()) {
            // 指向较大或者较小的子节点
            int tempMin = leftChildren(i);

            int rightIndex = tempMin + 1;
            //右子节点是否存在
            if (rightIndex < size()){
                // 最小堆, 如果右孩子比左孩子小,则更新tempMin指向
                if (isMinHeap && getValueByIndex(tempMin).compareTo(getValueByIndex(rightIndex)) > 0){
                    tempMin = rightIndex;
                }
                // 最大堆, 如果右孩子比左孩子大,则更新tempMin指向
                if (!isMinHeap && getValueByIndex(tempMin).compareTo(getValueByIndex(rightIndex)) < 0){
                    tempMin = rightIndex;
                }
            }

            // 与父节点比较判断是否需要交换,不需要则堆结构调整修复完毕。
            if (isMinHeap && getValueByIndex(i).compareTo(getValueByIndex(tempMin)) <= 0 ){
                break;
            }

            if (!isMinHeap && getValueByIndex(i).compareTo(getValueByIndex(tempMin)) >= 0 ){
                break;
            }

            //
            swapAndRestKeyIndexMap(keyTree,i,tempMin);

            //更新父节点
            i = tempMin;
        }
    }

    // 获取父节点
    private int parent(int index){
        return (index-1)/2;
    }

    // 获取左子节点
    private int leftChildren(int index){
        return index * 2 + 1;
    }

    //  获取➡又子节点
    private int rightChildren(int index){
        return index * 2 + 2;
    }


    // 交换索引堆中的索引i和j
    private void swap(ArrayList<KEY> arr, int i, int j) {
        KEY tmp = arr.get(i);
        arr.set(i,arr.get(j));
        arr.set(j,tmp);
    }

    // 交换索引堆中的索引i和j 并更新keyIndexMap的关系
    private void swapAndRestKeyIndexMap(ArrayList<KEY> arr, int i, int j) {
        KEY tmp = arr.get(i);
        arr.set(i,arr.get(j));
        arr.set(j,tmp);
        keyIndexMap.put(keyTree.get(i),i);
        keyIndexMap.put(keyTree.get(j),j);
    }

    /**
     * TopN计算封装
     * @param key
     * @param e
     */
    public void addForTopN(KEY key, VALUE e){
        if (maxSize == null || size() < maxSize){
            // 没指定阈值, 或者没达到阈值正常添加
            add(key,e);
            return;
        }

        // 指定了堆大小 并且 堆元素大小达到最大值需要,需要出堆一个
        Entry<KEY, VALUE> topEntry = peek();
        if (isMinHeap && e.compareTo(topEntry.getValue()) > 0){
            // 找到更大的数,添加到堆
            this.remove();
            this.add(key,e);
        }else if (!isMinHeap &&  e.compareTo(topEntry.getValue()) < 0){
            // 找到更小的数,添加到堆
            this.remove();
            this.add(key,e);
        }
    }
    
    /**
     *  查看堆情况
     */
    public void output(){
        List<Entry<KEY,VALUE>> ret = new ArrayList<>();
        for (KEY key : keyTree) {
            VALUE value = keyDataMap.get(key);
            ret.add(new Entry<>(key,value));
        }
        ret.sort(Comparator.comparing(Entry::getValue));
        ret.forEach(System.out::println);
    }
}

/**
 * @author burukeyou
 */
public class Entry<KEY,VALUE> {

    private KEY key;
    private VALUE value;

    public Entry(KEY key, VALUE value) {
        this.key = key;
        this.value = value;
    }

    public KEY getKey() {
        return key;
    }

    public VALUE getValue() {
        return value;
    }

    public void setKey(KEY key) {
        this.key = key;
    }

    public void setValue(VALUE value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "{" +
                "'key':" + key +
                ", 'value':" + value +
                '}';
    }
}

6.2 测试

堆排序

        // 创建最小索引堆   参数: true-最小堆, false-最大堆
        IndexHeap<Integer, String> indexMinHeap = new IndexHeap<>(true);

        // 往索引堆添加元素
        for (int i = 0; i < 10; i++) {
            indexMinHeap.add(i, "机器人"+i);
        }

        // 修改索引堆中索引为3的元素的值
        indexMinHeap.replace(3,"机器人9999999");

        // 查看堆顶元素
        Entry<Integer, String> peek = indexMinHeap.peek();

        // 出堆
        while (!indexMinHeap.isEmpty()){
            Entry<Integer, String> tmp = indexMinHeap.remove();
            System.out.println(tmp);
        }	

计算TopN

  • 有一个场景, 每天都有新用户添加, 初始会给每个用户分配幸运值, 需要实时计算幸运值排行前十的用户, 并且中途收到命令, 将此刻幸运值最高的用户 获得特殊奖励积分调到888888888。
        // 创建最小索引堆  设置堆容量10
        IndexHeap<Integer, Integer> indexMinHeap = new IndexHeap<>(true,10);

        // 1、新增一万个用户, 分配一个随机幸运值
        for (int i = 0; i <= 10000; i++) {
            indexMinHeap.addForTopN(i,new Random().nextInt(10000));
        }

        // 2、接到上级命令,将此刻幸运值最高的用户 获得特殊奖励, 幸运值调到888888888
        Integer maxUserId = indexMinHeap.peek().getKey(); // 积分最高的用户的id
        System.out.println("将用户: " + maxUserId + " 幸运值调到 888888888");
        indexMinHeap.replace(maxUserId,888888888);

        // 3、又新增了两万个用户
        for (int i = 10001; i <= 20000; i++) {
            indexMinHeap.addForTopN(i,new Random().nextInt(100000));
        }

        // 4、查询幸运值排行前十的用户
        System.out.println("=======幸运值排行如下: "+ LocalDateTime.now() +"===========");
        indexMinHeap.output();

        // 5、又新增了1万个用户
        for (int i = 20001; i <= 30000; i++) {
            indexMinHeap.addForTopN(i,new Random().nextInt(100000));
        }

        // 6、查询幸运值排行前十的用户
        System.out.println("=======幸运值排行如下: "+ LocalDateTime.now() +"===========");
        indexMinHeap.output();
举报

相关推荐

0 条评论