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

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

本篇文章我们 手写加强堆

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

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 难题

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

相关推荐
吾日三省吾码4 小时前
JVM 性能调优
java
LNTON羚通4 小时前
摄像机视频分析软件下载LiteAIServer视频智能分析平台玩手机打电话检测算法技术的实现
算法·目标检测·音视频·监控·视频监控
弗拉唐5 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi775 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3436 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀6 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
哭泣的眼泪4086 小时前
解析粗糙度仪在工业制造及材料科学和建筑工程领域的重要性
python·算法·django·virtualenv·pygame
蓝黑20206 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深6 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++