Heap
二叉堆
1.设计堆的目的
设计一种数据结构,用来存放整数,要求提供 3 个接口:
- 添加元素
- 获取最大值
- 删除最大值
用已学过的数据结构来对比一下时间复杂度:
为了迎合此需求出现了一种新的数据结构 —— 堆
- 获取最大值: O(logn)O(logn)
- 删除最大值:O(logn)O(logn)
- 添加元素:O(logn)
2. 堆的种类
堆(Heap)是一种 树状 的数据结构,常见的堆实现有:
- 二叉堆(Binary Heap,完全二叉堆)
- 多叉堆(D-heap、D-ary Heap)
- 索引堆(Index Heap)
- 二项堆(Binomial Heap)
- 斐波那契堆(Fibonacci Heap)
- 左倾堆(Leftist Heap,左式堆)
- 斜堆(Skew Heap)
堆的重要性质
任意节点的值总是 ≥ \geq≥ ( ⩽ \leqslant⩽) 子节点的值
最大堆和最小堆
- 任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆
- 任意节点的值总是 ⩽ 子节点的值,称为:最小堆、小根堆、小顶堆
堆中的元素必须具备可比较性(跟二叉搜索树一样)
3. 二叉堆(Binary Heap)
3.1 二叉堆的数组实现逻辑
- 二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆
- 完全二叉树的特点是层序遍历的时候,节点都是挨着的,根据这个特点,只要能根据索引就可以找到一个节点的父节点、左子节点、右子节点。因此可以用数组来实现完全二叉树
一颗 n个节点的完全二叉树中,索引为i的节点的父子关系:
- i = 0 为根节点
- 左子节点索引:2i + 1
- 右子节点索引:2i + 2
- 当 2i + 1 > n - 1 ,此节点无左子节点
- 当 2i + 2 > n - 1 ,此节点无右子节点
- 当i > 0时,父 节点的索引为 floor( (i - 1) / 2 )
3.2 二叉堆对外提供的接口
public interface Heap<E> {
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void add(E element); // 添加元素
E get(); // 获得堆顶元素
E remove(); // 删除堆顶元素
E replace(E element); // 删除堆顶元素的同时插入一个新元素
}
3.3 添加实现
思路:
- 首先将新节点放入数组的最后一位
- 循环执行上滤操作(Sift Up)
上滤操作:
- 如果 node > 父节点,与父节点交换位置
- 如果 node <= 父节点,或者 node 没有父节点,退出循环
图示:
假设当前添加新节点80,其过程如上图所示;
时间复杂度和层数有关,因此为O(logn)
private void elementNotNullCheck(E element){
if(element == null){
throw new RuntimeException("element can not be null");
}
}
private void ensureCapacity(int size){
if(size == 0){
elements = (E[])new Object[DEFAULT_CAPACITY];
}
if(size == elements.length){
int oldCapacity = size;
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for(int i = 0;i<size;i++){
newElements[i] = elements[i];
}
elements = newElements;
}
}
private void swift(int i,int j){
E tmp = elements[i];
elements[i] = elements[j];
elements[j] = tmp;
}
@Override
public void add(E element) {
elementNotNullCheck(element);
ensureCapacity(size + 1);
elements[size++] = element;
siftUp(size - 1);
}
//logN
private void siftUp(int index){
int parentIndex = (index - 1) >> 1;
while(parentIndex >= 0){
if(compare(elements[index],elements[parentIndex]) > 0){
swift(index,parentIndex);
index = parentIndex;
parentIndex = (index - 1) >> 1;
}else{
break;
}
}
}
3.4 上滤操作优化
思路:
- 如果父节点比新节点小,copy 父节点内容到当前节点;
- 最后找到目标index,才插入进去;
- 减少了交换过程
图示:
private void siftUp(int index){
//todo 找应该放置的位置
E cur = elements[index];
//当index > 0的时候才有父节点
while(index > 0){
int parentIndex = (index - 1) >> 1;
E parent = elements[parentIndex];
if(compare(cur,parent) <= 0) break;
//break说明找到位置 就是index的位置
elements[index] = parent;
index = parentIndex;
}
//要么index = 0 要么 break出来的
elements[index] = cur;
}
3.5 删除
思路:
- 删除操作先将数组中最后一个元素覆盖第一个元素;
- 由于覆盖后,可能破坏了堆的性质,因此需要将这个元素下滤
- 下滤就是将子结点中最大的元素和当前元素交换;
图示:
时间复杂度:O(logn)
public void siftDown(int index) {
E old = elements[index];
while (index <= (size - 2) >> 1) {
int leftIndex = (index << 1) + 1;
int rightIndex = leftIndex + 1;
int maxIndex = leftIndex;
if (index <= (size - 3) >> 1) {//左右都有
maxIndex = compare(elements[leftIndex], elements[rightIndex]) > 0 ? leftIndex : rightIndex;
}
if (compare(elements[maxIndex], old) > 0) {
elements[index] = elements[maxIndex];
index = maxIndex;
} else {
break;
}
}
elements[index] = old;
}
@Override
public E remove() {
emptyCheck();
E old = elements[0];
elements[0] = elements[size - 1];
elements[size - 1] = null;
size--;
siftDown(0);
return old;
}
3.6 replace
删除堆顶元素并插入一个新的元素
@Override
public E replace(E element) {
elementNotNullCheck(element);
E old = null;
if (size == 0) {
elements[0] = element;
size ++;
}else{
old = elements[0];
elements[0] = element;
siftDown(0);
}
return old;
}
3.6 批量建堆
批量建堆指的是给定一个任意数组,将此数组建立成堆
老土方法
自上而下的上滤
图中红色的节点是需要执行上滤操作的;判断条件是只要有父节点就需要上滤;
自下而上的下滤
图中红色的节点是需要执行下滤操作的;判断条件是只要有子节点就需要下滤;
效率对比:
- 自上而下的上滤时间复杂度:O ( nlogn )
- 自下而上的下滤时间复杂度:O(nlogk)
自下而上的下虑效率要高一些;我们可以从上面这幅图观察出来,层越低,该层的节点数目越多;自上而下的上滤则存在大量的节点从最底层要比较到顶层;而自下而上的下虑,只有顶部的少数节点需要从顶层比较到底层;
3.7 全部代码
接口
package 二叉堆;
public interface Heap<E> {
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void add(E element); // 添加元素
E get(); // 获得堆顶元素
E remove(); // 删除堆顶元素
E replace(E element); // 删除堆顶元素的同时插入一个新元素
}
抽象类
package 二叉堆;
import java.util.Comparator;
public abstract class AbstractHeap<E> implements Heap<E> {
protected int size;
protected Comparator<E> comparator;
public AbstractHeap(){}
public AbstractHeap(Comparator<E> comparator){
this.comparator = comparator;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
}
实现类
package 二叉堆;
import java.util.Comparator;
public class BinaryHeap<E> extends AbstractHeap<E> {
private E[] elements;
private static final int DEFAULT_CAPACITY = 10;
public BinaryHeap(E[] elements, Comparator<E> comparator) {
super(comparator);
if (elements == null || elements.length == 0) {
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
} else {
size = elements.length;
int capacity = Math.max(elements.length, DEFAULT_CAPACITY);
this.elements = (E[]) new Object[capacity];
for (int i = 0; i < elements.length; i++) {
this.elements[i] = elements[i];
}
heapify();
}
}
public BinaryHeap(E[] elements) {
this(elements, null);
}
public BinaryHeap(Comparator<E> comparator) {
this(null, comparator);
}
public BinaryHeap() {
this(null, null);
}
private int compare(E e1,E e2){
if(comparator == null){
return ((Comparable<E>)e1).compareTo(e2);
}else{
return comparator.compare(e1,e2);
}
}
private void nullCheck(E e){
if(e == null){
throw new RuntimeException("param can not be null");
}
}
@Override
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
private void ensureCapacity(int size){
if(size > elements.length){
E[] newArr = (E[])new Object[elements.length << 1];
for (int i = 0; i < size; i++) {
newArr[i] = elements[i];
}
elements = newArr;
}
}
@Override
public void add(E element) {
nullCheck(element);
ensureCapacity(size+1);
elements[size++] = element;
siftUp(size - 1);
}
private void siftUp(int index){
//todo 找应该放置的位置
E cur = elements[index];
//当index > 0的时候才有父节点
while(index > 0){
int parentIndex = (index - 1) >> 1;
E parent = elements[parentIndex];
if(compare(cur,parent) <= 0) break;//找到位置 就是index的位置
elements[index] = parent;
index = parentIndex;
}
elements[index] = cur;
}
@Override
public E get() {
emptyCheck();
return elements[0];
}
private void emptyCheck(){
if(size == 0){
throw new RuntimeException("empty heap");
}
}
@Override
public E remove() {
//用最后一个节点覆盖根结点
//删除最后一个节点
//下虑 和 最大的子节点互换
emptyCheck();
E root = elements[0];
int lastIndex = --size;
elements[0] = elements[lastIndex];
elements[lastIndex] = null;
siftDown(0);
return root;
}
private void siftDown(int index){
E cur = elements[index];
int haveChild = (size - 2) >> 1;
//index <= haveChild 有子节点
while(index <= haveChild){
int leftchildIndex = (index << 1) + 1;//左子节点
int rightChildIndex = leftchildIndex + 1;//右子节点
int maxChildIndex = leftchildIndex;
//有右子节点的时候
if( rightChildIndex < size ){
maxChildIndex = compare(elements[leftchildIndex],elements[rightChildIndex]) > 0? leftchildIndex:rightChildIndex;
}
E maxchild = elements[maxChildIndex];
if(compare(cur,maxchild) > 0) break; //index 就是目标位置
elements[index] = maxchild;
index = maxChildIndex;
}
elements[index] = cur;
}
/**
* 删除堆顶元素的同时插入一个新元素
* @param element
* @return
*/
@Override
public E replace(E element) {
nullCheck(element);
E root = null;
if (size == 0) {
elements[0] = element;
size++;
} else {
root = elements[0];
elements[0] = element;
siftDown(0);
}
return root;
}
/**
* 批量建堆
*/
private void heapify() {
// 自上而下的上滤
// for (int i = 1; i < size; i++) {
// siftUp(i);
// }
// 自下而上的下滤
for (int i = (size >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
}
public static void main(String[] args) {
Integer[] arr = {22,54,13,57,1,5,8,4,3,46,12,47,23};
BinaryHeap<Integer> heap = new BinaryHeap<Integer>(arr, (o1,o2) -> o2-o1);
System.out.println(heap.get());
}
}
3.8 构建一个最小堆
在写完最大堆以后,实现最小堆不需要修改源代码,只需要在创建堆时,传入与最大堆比较方式相反的比较器即可。
4.堆的应用
4.1 TOP K 问题
什么是 TopK 问题
从 n 个整数中,找出最大的前 k 个数(k << n)
例如:从100万个整数中找出最大的100个整数
- 如果使用排序算法进行全排序,需要 O ( n l o g n ) 的时间复杂度
- 如果使用二叉堆来解决,可以使用 O ( n l o g k )的时间复杂度来解决
TOPK 实现方法
(1)新建一个小顶堆,扫描 n 个整数先将遍历到的前 k 个数放入堆中
(2)从第 k+1 个数开始,如果大于堆顶元素,就使用 replace 操作
(删除堆顶元素,将第k+1个数添加到堆中)
(3)扫描完毕后,堆中剩下的就是最大的前 k 个数
public static void main(String[] args) {
// 新建一个小顶堆
BinaryHeap<Integer> heap = new BinaryHeap<>(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
// 找出最大的前k个数
int k = 3;
Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93,
91, 19, 54, 47, 73, 62, 76, 63, 35, 18,
90, 6, 65, 49, 3, 26, 61, 21, 48};
for (int i = 0; i < data.length; i++) {
if (heap.size() < k) { // 前k个数添加到小顶堆
heap.add(data[i]); // logk
} else if (data[i] > heap.get()) { // 如果是第 k + 1 个数,并且大于堆顶元素
heap.replace(data[i]); // logk
}
}
// O(nlogk)
}
如果是找出最小的前 k 个数呢?
- 用大顶堆
- 如果小于堆顶元素,就使用 replace 操作