上一篇文章我们介绍了有关堆排序、大根堆、小根堆的内容,(还没看过上篇文章的赶快 点我 查看哦!)
本篇文章我们 手写加强堆 !
有小伙伴可能就 有疑惑 了:
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);
}
在上一篇文章的堆排序中,我们已经介绍并实现了大根堆的 heapInsert
和 heapfiy
方法。那这次就将其改为 小根堆 的实现:
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 难题!
~ 点赞 ~ 关注 ~ 不迷路 ~!!!