文章目录
1.0 堆的概念
1.1堆的使用
假如我们需要设计一种数据接口,来实现一组数据的存储,并实现三个功能:
- 获取最大元素
- 删除最大元素
- 添加元素
我们可以采用什么数据结构?
| 数据结构 | 获取最大值 | 删除最大值 | 添加元素 | |
|---|---|---|---|---|
| 动态数组/双向链表 | O(n) | O(n) | O(1) | |
| 有序的动态数组/双向链表 | O(1) | O(1) | O(n) | 全排序有些浪费 |
| BBST | O(logn) | O(logn) | O(logn) | 杀鸡用了牛刀 |
| 堆 | O(1) | O(logn) | O(logn) | 比较合适 |
1.2 堆的结构
堆也是一种树状的数据结构,常见的堆有
- 二叉堆(Binary Heap,完全二叉堆)
- 多叉堆(D-heap、D-ary Heap)
- 索引堆(Index Heap)
- 二项堆(Binomial Heap)
- 斐波那契堆(Fibonacci Heap)
- 左倾堆(Leftist Heap,左式堆)
- 斜堆(Skew Heap)

堆的一个重要性质:任意节点的值总是 ≥( ≤ )子节点的值。如果任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆。如果任意节点的值总是 ≤ 子节点的值,称为:最小堆、小根堆、小顶堆。由此可见,堆中的元素必须具备可比较性(跟二叉搜索树一样)。
1.3堆的基本接口
| 接口 | 功能 |
|---|---|
| int size() | 元素数量 |
| boolean isEmpty() | 是否为空 |
| void clear() | 清空堆 |
| void add(E element) | 添加元素 |
| E remove() | 删除最大值,返回被删除的值 |
| E replace(E element) | 删除最大值,添加元素element |
2.0 二叉堆
2.1 二叉堆的结构
二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆。


鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现即可。且不难发现,父子节点之间的索引存在一些关系,假设当前节点的索引为index:
- 根节点索引为0
- 父节点索引为floor((index-1)/2),floor为向下取整
- 如果2 * index+1<size,那么存在左子节点,且索引为2*index+1
- 3如果2 * index+2<size,那么存在右子节点,且索引为2*index+2
2.2 二叉堆的具体实现
2.2.1 创建二叉堆
定义Heap接口
public interface Heap<E>{
int size();
boolean isEmpty();
void clear();
void add(E element);
/**
* //删除最大元素
* @return 被删除的元素
*/
E remove();
/**
* //获取堆顶元素
* @return
*/
E get();
/**
* //加入element,删除最大元素
* @param element
* @return 原最大元素
*/
E replace(E element);
}
定义二叉堆
public class BinaryHeap<E> implements Heap<E> {
private int size;
private E[] elements;//定义存放元素的数组
private Comparator<E> comparator;
private static final int DEFAULT_CAPACITY=10;
public BinaryHeap(Comparator<E> comparator){
this.comparator=comparator;
}
public BinaryHeap(){
this(null);
}
//实现接口...
}
2.2.1 get方法
get方法获取最大元素,而在最大二叉堆(大顶堆)中最大元素就是索引为0的元素,此外在获取最大元素时还需要判断堆是否为空。
@Override
public E get(){
emptyCheck();
return elements[0];
}
2.2.2 add方法
在添加时需要对元素进行比较,因此我们需要一个compare方法。
private int compare(E e1,E e2){//比较方法
return comparator!=null?comparator.compare(e1,e2):
((Comparable<E>)e1).compareTo(e2);
}
在添加操作中,我们先将新元素放置在末尾的位置,此时最大堆所有子元素都比父元素小的原则可能被破坏,此时我们采用上滤(sift up)的操作将该原则恢复。

上滤操作:将节点与父节点比较,如果比父节点大,则交换位置,然后再次比较,交换位置…直到不再比父节点大或者到达堆顶。



实现代码
@Override
public void add(E element) {
elementNotNullCheck(element);
ensureCapacity(size+1);
elements[size++]=element;//插入数组末位
siftUP(size-1);
}
/**
* 对索引为index处的节点进行上滤
*/
private void siftUP(int index){
E element=elements[index];
while(index>0){//当节点到达堆顶(索引为0)无需再上滤
int pIndex=(index-1)/2;
E parent=elements[pIndex];
if(compare(element,parent)<=0)
break;
//父节点下移
elements[index]=parent;
//更改索引
index=pIndex;
}
elements[index]=element;
}
2.2.3 remove方法
remove方法需要删除堆中的最大元素,而直接删除elements[0]将导致堆顶元素空缺,在此的解决方法是,用最末端的元素代替堆顶元素,此时再对堆顶元素进行下滤操作,二叉堆即可恢复。

使用末尾元素代替堆顶元素

进行下滤:将当前元素与子元素进行比较,如果存在大于当前元素的值,则使用最大的子元素与当前元素交换,并不断的进行该操作,直到(1)不再有子元素,即当前元素所在节点为最后一个非叶子节点(2)不存在子元素大于当前元素。


此时不存在比44更大的子元素了,下滤结束。

在完成下滤方法的代码时,我们需要考虑一个问题,如何获取最后一个非叶子节点的索引?(这是下滤操作循环结束的条件之一),其实对于完全二叉树叶子节点数与非叶子节点数和n之间存在着一定的关系。
我们用 n0、n1、n2 分别表示叶子节点、有一个子节点的叶子、有两个子节点的节点的数量,那么n0+n1+n2=n。
再考虑一棵树的边数=n-1,有两个子节点的节点有2条边,有一个子节点的节点有1条边,所以 2 * n2+n1=n-1。
将两式子联立:2n2+n1+1=n2+n1+n0,有n2=n0-1,带入上式有 n=2n0-1+n1,而对于完全二叉树,n1只能是0或1。那么当n为偶数时,n0=n/2;当n为奇数时,n0=(n+1)/2=ceiling(n/2),ceiling表示向上取整,综合两者,n0=ceiling(n/2),那么非叶子节点的个数为 n1+n2=n-n0=floor(n/2),floor表示向下取整。
所以非叶子节点的个数为n/2(自动向下取整)。
实现代码:
@Override
public E remove() {
emptyCheck();
E root=elements[0];
//1.让最小元素代替最大元素
int lastIndex=--size;
elements[0]=elements[lastIndex];
elements[lastIndex]=null;
siftDown(0);
return root;
}
/**
*对索引为index处的节点进行下滤
*/
private void siftDown(int index){
//2.向下迭代,找到子节点中最大进行替换
E element=elements[index];
int half=size/2;//非叶子节点的数量
while(index<half){//当有叶子节点时,可以考虑下滤
//将左节点作为默认节点,一定有左节点
int childIndex=(index<<1)+1;
E child=elements[childIndex];
//获取右节点
int rightIndex=childIndex+1;
if(rightIndex<size&&compare(elements[rightIndex],child)>0){
child=elements[childIndex=rightIndex];
}
if(compare(child,element)<=0)//子节点小于等于当前节点
break;
elements[index]=child;
//修改index
index=childIndex;
}
elements[index]=element;
}
2.2.4 replace方法
replace方法与remove方法十分相似,只需将新元素代替原堆顶元素,再下滤即可。
@Override
public E replace(E element) {
elementNotNullCheck(element);
E root=null;
elements[0]=element;
if(isEmpty()){
size++;
}else{
siftDown(0);
}
return root;
}
2.2.5 整体代码
import java.util.Comparator;
public class BinaryHeap<E> implements Heap<E>{
private int size;
private E[] elements;
private Comparator<E> comparator;
private static final int DEFAULT_CAPACITY=10;
public BinaryHeap(){
this(null,null);
}
public BinaryHeap(Comparator<E> comparator){
this(null,comparator);
}
public BinaryHeap(E[] elements,Comparator<E> comparator){
this.comparator=comparator;
if(elements==null||elements.length==0){
this.elements=(E[]) new Object[DEFAULT_CAPACITY];
}else{
size=elements.length;
int capacity=size<DEFAULT_CAPACITY?DEFAULT_CAPACITY:size;
this.elements= (E[]) new Object[capacity];
for(int i=0;i<size;i++)//深拷贝
this.elements[i]=elements[i];
heapify();
}
}
public BinaryHeap(E[] elements){
this(elements,null);
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size==0;
}
@Override
public void clear() {
for(int i=0;i<size;i++)
elements[i]=null;
size=0;
}
@Override
public void add(E element) {
elementNotNullCheck(element);
ensureCapacity(size+1);
elements[size++]=element;//插入数组末位
siftUP(size-1);
}
@Override
public E remove() {
emptyCheck();
E root=elements[0];
//1.让最小元素代替最大元素
int lastIndex=--size;
elements[0]=elements[lastIndex];
elements[lastIndex]=null;
siftDown(0);
return root;
}
@Override
public E get() {
emptyCheck();
return elements[0];
}
@Override
public E replace(E element) {
elementNotNullCheck(element);
E root=null;
elements[0]=element;
if(isEmpty()){
size++;
}else{
siftDown(0);
}
return root;
}
private int compare(E e1,E e2){//比较方法
return comparator!=null?comparator.compare(e1,e2):
((Comparable<E>)e1).compareTo(e2);
}
/**
* 当堆为空时,尝试获取堆顶将发生异常
*/
private void emptyCheck(){
if(size==0)
throw new ArrayIndexOutOfBoundsException("Heap is empty");
}
private void elementNotNullCheck(E element){
if(element==null)
throw new IllegalArgumentException("Element must be not null");
}
private void ensureCapacity(int neededCapacity){
int oldCapacity=elements.length;
if(neededCapacity<=oldCapacity)//当前数组大小满足条件
return;
int newCapacity=oldCapacity+(oldCapacity>>1);//扩容1.5倍
System.out.println("original size:"+oldCapacity+"-new size:"+newCapacity);
E[] newElements=(E[])new Object[newCapacity];
System.arraycopy(elements, 0, newElements, 0, oldCapacity);
elements=newElements;
}
/**
* 对索引为index处的节点进行上滤
*/
private void siftUP(int index){
E element=elements[index];
while(index>0){//当节点到达堆顶(索引为0)无需再上滤
int pIndex=(index-1)/2;
E parent=elements[pIndex];
if(compare(element,parent)<=0)
break;
//父节点下移
elements[index]=parent;
//更改索引
index=pIndex;
}
elements[index]=element;
}
/**
*对索引为index处的节点进行下滤
*/
private void siftDown(int index){
//2.向下迭代,找到子节点中最大进行替换
E element=elements[index];
int half=size/2;//非叶子节点的数量
while(index<half){//当有叶子节点时,可以考虑下滤
//将左节点作为默认节点,一定有左节点
int childIndex=(index<<1)+1;
E child=elements[childIndex];
//获取右节点
int rightIndex=childIndex+1;
if(rightIndex<size&&compare(elements[rightIndex],child)>0){
child=elements[childIndex=rightIndex];
}
if(compare(child,element)<=0)//子节点小于等于当前节点
break;
elements[index]=child;
//修改index
index=childIndex;
}
elements[index]=element;
}
/**
* 批量建堆
*/
private void heapify(){
//方案一:从上往下的上滤
//相当于插入操作,一个一个插入,时间复杂度(nlog(n))
// for(int i=1;i<size;i++)
// siftUP(i);
//方案二:从下往上的下滤
//类似删除操作,将左右都维护为堆,再将顶部下滤,进而整体为堆
for(int i=(size>>1)-1;i>=0;i--)//非叶子节点数量:floor(size/2)
siftDown(i);
}
}
3.0 top-k问题
top-k问题:从 n 个整数中,找出最大的前 k 个数( k 远远小于 n )。
如果使用排序算法进行全排序,需要 O(nlogn) 的时间复杂度,但如果使用二叉堆来解决,可以使用 O(nlogk) 的时间复杂度来解决。
解决思路:使用最小堆,依次遍历n个整数,将前k个整数放入最小堆中,对于之后的整数,如果小于堆顶元素(堆中最小的元素),则无需处理,直接跳过(小于了已知的k个元素),如果大于堆顶元素,则用其代替堆顶元素,并对堆进行维护(即replace方法),如此往复,直至遍历完n个数。最后存储在堆中的就是最大的前k个数。
//top-K问题(使用小顶堆解决求最大k个值问题,时间复杂度 nlog(k) )
//使用排序算法时间复杂度:nlog(n)
public static void test3(int k){
Integer[] elements={2,34,234,5,53,78,65,43,775,678,564,22};
BinaryHeap<Integer> heap=new BinaryHeap(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
//遍历数组
for(int i=0;i<elements.length;i++){
if(heap.size()<k){
heap.add(elements[i]);
}else{
if(elements[i]>heap.get())
heap.replace(elements[i]);
}
}
while(!heap.isEmpty()){
System.out.print(heap.remove()+" ");
}
System.out.println();
}
2136





