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

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

本篇文章我们 手写加强堆

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

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

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

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

根本原因无反向索引表

正因为系统所提供的堆在底层是由数组实现的,只能通过下标找到值不能反向通过值找到当前对象存在的位置,因此需要遍历寻找,时间复杂度就退化为了 O ( N ) O(N) O(N)
【堆 - 专题】系统已经提供了“堆”,为什么还要手写?_第1张图片


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

// 手写实现加强堆
// 结构体定义
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 控制堆的大小;由于使用了泛型 因此需要使用 比较器 自定义排序方式。

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

// 判空
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 方法。那这次就将其改为 小根堆 的实现:

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 就和普通的交换函数 不一样 了。不仅仅需要交换对象信息,要交换对象的 反向索引表 同样 需要更新 :

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);
}

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

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

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

通过以上函数功能的实现,寻找某元素时,不需要先遍历整个数组,可以直接进行增删改查的操作,时间复杂度控制在了 O ( l o g N ) O(log N) O(logN) 以内!

你学会了么?

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

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

你可能感兴趣的:(java,算法,数据结构)