堆的概念与二叉堆的实现详细图解

1.0 堆的概念

1.1堆的使用

假如我们需要设计一种数据接口,来实现一组数据的存储,并实现三个功能:

  1. 获取最大元素
  2. 删除最大元素
  3. 添加元素

我们可以采用什么数据结构?

数据结构获取最大值删除最大值添加元素
动态数组/双向链表O(n)O(n)O(1)
有序的动态数组/双向链表O(1)O(1)O(n)全排序有些浪费
BBSTO(logn)O(logn)O(logn)杀鸡用了牛刀
O(1)O(logn)O(logn)比较合适

1.2 堆的结构

堆也是一种树状的数据结构,常见的堆有

  1. 二叉堆(Binary Heap,完全二叉堆)
  2. 多叉堆(D-heap、D-ary Heap)
  3. 索引堆(Index Heap)
  4. 二项堆(Binomial Heap)
  5. 斐波那契堆(Fibonacci Heap)
  6. 左倾堆(Leftist Heap,左式堆)
  7. 斜堆(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:

  1. 根节点索引为0
  2. 父节点索引为floor((index-1)/2),floor为向下取整
  3. 如果2 * index+1<size,那么存在左子节点,且索引为2*index+1
  4. 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();
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值