【堆 - 专题】系统已经提供了“堆”,为什么还要手写?

上一篇文章我们介绍了有关堆排序、大根堆、小根堆的内容,(还没看过上篇文章的赶快 点我 查看哦!)

本篇文章我们 手写加强堆

有小伙伴可能就 有疑惑 了:

Java 中的 java.util.PriorityQueue 类提供了优先级队列的实现,内部使用来维护元素的优先级顺序。那么就可以使用 PriorityQueue 类来很方便地实现优先级队列。那为什么还要自己 手动实现 一个堆呢?

答案很简单,系统所提供的堆 功能不全面。常用的函数有:

方法名 功能介绍
boolean offer(E e) 插入元素 e ,插入成功返回 true。 如果 e 为空,抛出 NullPointerException 异常
E peek() 获得堆顶元素,堆为空返回 null
E poll() 移除堆顶元素并返回,堆为空返回 null
int size() 获得堆中元素个数
void clear() 清空
boolean isEmpty() 判断堆是否为空,为空返回 true

假设一种场景需要频繁的删除某个对象或修改某个对象的属性,使用系统提供的堆怎样高效的完成呢?详细一点说,系统提供的堆 存在以下问题:

  • 已经入堆的元素,如果参与排序的方法发生了变化。系统提供的堆无法在 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g N ) O(logN) </math>O(logN) 的时间复杂度下进行调整!只能以 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N) 进行调整。
  • 系统提供的堆只能弹出堆顶,不能在 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g N ) O(logN) </math>O(logN) 的时间复杂度内随意删除任何一个堆中的元素!一定会高于 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g N ) O(logN) </math>O(logN)。

根本原因无反向索引表

正因为系统所提供的堆在底层是由数组实现的,只能通过下标找到值不能反向通过值找到当前对象存在的位置 ,因此需要遍历寻找,时间复杂度就退化为了 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)。


找到了问题的根源,也就迎刃而解了 ------ 添加反向索引表

java 复制代码
// 手写实现加强堆
// 结构体定义
public class HeapGreater<T> {

    private ArrayList<T> heap;
    private HashMap<T, Integer> indexMap;
    private int heapSize;
    private Comparator<? super T> comp;

    public HeapGreater(Comparator<? super T> c) {
        heap = new ArrayList<>();
        indexMap = new HashMap<>();
        heapSize = 0;
        comp = c;
    }
}

使用 ArrayList 数组实现堆 heap;添加反向索引表 indexMap 用于快速定位某个元素的下标位置;heapSize 控制堆的大小;由于使用了泛型 <T> 因此需要使用 比较器 自定义排序方式。

下面实现几个比较简单的功能(不改变堆中元素):判空、堆大小、是否存在特定对象、取堆顶元素

java 复制代码
// 判空
public boolean isEmpty() {
    return heapSize == 0;
}

// 返回当前堆的大小
public int size() {
    return heapSize;
}

// 判断 obj 是否存在于堆中
public boolean contains(T obj) {
    return indexMap.containsKey(obj);
}

// 获取堆顶元素
public T peek() {
    return heap.get(0);
}

在上一篇文章的堆排序中,我们已经介绍并实现了大根堆的 heapInsertheapfiy 方法。那这次就将其改为 小根堆 的实现:

java 复制代码
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(best, index);
        index = best;
        left = index * 2 + 1;
    }
}

此时的交换函数 swap 就和普通的交换函数 不一样 了。不仅仅需要交换对象信息,要交换对象的 反向索引表 同样 需要更新 :

java 复制代码
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);
}

接下来,我们来实现需要对堆中元素进行 变化 的一系列函数。包括:添加、弹出、移除

java 复制代码
// 加入元素
public void push(T obj) {
    heap.add(obj);
    indexMap.put(obj, heapSize);
    heapInsert(heapSize++);
}

// 弹出堆顶元素
public T pop() {
    T ans = heap.get(0);
    swap(0, heapSize - 1);
    indexMap.remove(ans);
    heap.remove(--heapSize);
    heapify(0);
    return ans;
}

// 移除指定元素
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));
}

push() 添加元素时,先插入到堆底,再进行 heapInsert 操作进行调整,heapSize++

pop() 弹出元素时,堆顶元素与最后一个元素交换,heapSize--, 再将堆顶元素 heapfiy 进行调整。

remove() 移除元素时,同样使用最后一个元素进行代替。找到要移除的元素下标,并在反向索引表中移除。当移除元素不是最后一个元素时,替换位置用最后一个元素顶替。因为不确定元素大小,因此不知道需要 上调 还是 下调,两者均调用,但最多只会执行其中一个。

进行上述几个操作时 一定记得更改 反向索引表 里的值哦!

通过以上函数功能的实现,寻找某元素时,不需要先遍历整个数组,可以直接进行增删改查的操作,时间复杂度控制在了 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g N ) O(log N) </math>O(logN) 以内!

你学会了么?

下篇文章我们继续对 加强堆 做进一步深入的理解,解决 TopK 难题

~ 点赞 ~ 关注 ~ 不迷路 ~!!!

相关推荐
Kenneth風车1 分钟前
【机器学习(九)】分类和回归任务-多层感知机(Multilayer Perceptron,MLP)算法-Sentosa_DSML社区版 (1)11
算法·机器学习·分类
最后一个bug6 分钟前
rt-linux中使用mlockall与free的差异
linux·c语言·arm开发·单片机·嵌入式硬件·算法
Q_192849990618 分钟前
基于Spring Boot的个人健康管理系统
java·spring boot·后端
蹉跎x1 小时前
力扣1358. 包含所有三种字符的子字符串数目
数据结构·算法·leetcode·职场和发展
m0_748245171 小时前
Web第一次作业
java
小码的头发丝、1 小时前
Java进阶学习笔记|面向对象
java·笔记·学习
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
rainoway1 小时前
CRDT宝典 - yata算法
前端·分布式·算法
坊钰2 小时前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表