数据结构-二叉堆

102 阅读4分钟

27c372a9725d7b59.webp

说起堆,有过编程经验的小伙伴可能会想起来是什么,那么二叉堆是什么呢?有什么用呢?我们自己动手如何实现一个呢?带着这些疑问,开始~

是什么?

  • 是一个特殊的堆,是一棵完全二叉树或者是近似完全二叉树(二叉树,上篇文章详细介绍过)
  • 每个节点的左子树和右子树都是一个二叉堆
  • 当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆
  • 二叉堆一般用数组来表示。如果根节点在数组中的位置是1,第n个位置的子节点分别在2n和 2n+1。因此,第1个位置的子节点在2和3,第2个位置的子节点在4和5。以此类推。这种基于1的数组存储方式便于寻找父节点和子节点
  • 下图所示是一个二叉堆(最大堆),用数组表示:11 9 10 5 6 7 8 1 2 3 4

image.png

实操

以最大堆为例子,探索一下对其的基本操作

添加元素

  • 以上图中的例子,假如添加一个元素15。
  • 我们知道其底层使用数组来存储,那么添加一个元素,也即在数组的最后添加一个元素,也即在节点7的左子树添加15元素,结果如下图所示:

image.png

  • 根据最大堆的性质,父节点要大于其左右子树,7不大于新添加的15,所以这里存在一个操作,也即与父节点进行比较,如果新添加的元素大于其父节点,那么就和父节点交换位置。交换之后继续与其父节点比较,直到满足为止
  • 在这个例子中,7小于15 所以7和15 要转换位置:

image.png

  • 转换之后15与10比较,10小于15,所以需要转换位置
  • 转换之后,15和11比较,11小于15,再次转换位置
  • 到此,15变成整个树的根节点,满足二叉堆的性质,添加结束,结果如图:

image.png

取出最大的元素(删除最大元素)

将元素15取出,也即将整棵树的根节点删除,其左右子树如何操作呢?

  • 这里遵循的方法原则是将存储的数组中最后一个元素,也即二叉堆中元素7顶替元素15的位置,然后依次比较以满足最大堆性质,移动之后结果:

image.png

  • 元素7分别于左右子树中最大的比较也即与11比较,发现7小于11,那么元素7与元素11调换位置
  • 调换之后继续与左右子树中最大值比较,也即与元素10比较,发现7小于10,那么元素7与元素10调换位置
  • 到此调整完成,满足最大堆性质:

image.png

代码实操

以java代码为例实现最大堆

//因为存在比较的过程,所以实现Comparable接口
//底层使用数组来存储元素
public class MaxHeap<E extends Comparable<E>> {

    private Array<E> data;

    public MaxHeap(int capacity){
        data = new Array<>(capacity);
    }

    public MaxHeap(){
        data = new Array<>();
    }

    // 返回堆中的元素个数
    public int size(){
        return data.getSize();
    }

    // 返回一个布尔值, 表示堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
    private int parent(int index){
        if(index == 0){
            throw new IllegalArgumentException("没有父节点");
        }
        return (index - 1) / 2;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return index * 2 + 1;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return index * 2 + 2;
    }

    // 向堆中添加元素
    public void add(E e){
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    //元素上移操作,上面理论部分详细解释过这个过程,小伙伴们可以结合这个递归函数体会一下
    private void siftUp(int k){
        while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){
            data.swap(k, parent(k));
            k = parent(k);
        }
    }

    // 看堆中的最大元素
    public E findMax(){
        if(data.getSize() == 0){
            throw new IllegalArgumentException("无数据");
        }
        return data.get(0);
    }

    // 取出堆中最大元素
    //最大的元素也即整个树的根节点,也就是数组的头元素
    //也即删除最大的元素,上面理论部分有详细的步骤说明删除之后,元素如何调整
    //结合代码再次体会
    public E extractMax(){

        E ret = findMax();

        data.swap(0, data.getSize() - 1);
        data.removeLast();
        siftDown(0);

        return ret;
    }

    //元素下移操作
    private void siftDown(int k){

        while(leftChild(k) < data.getSize()){
            int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置
            if( j + 1 < data.getSize() &&
                    data.get(j + 1).compareTo(data.get(j)) > 0 ){
                j ++;
            }
            // data[j] 是 leftChild 和 rightChild 中的最大值

            if(data.get(k).compareTo(data.get(j)) >= 0 ){
                break;
            }

            data.swap(k, j);
            k = j;
        }
    }
}

  • 结合代码与理论可以体会其中实现的细节,也能再次体会递归这个方法的妙用
  • 感兴趣小伙伴可以自己尝试使用任意一个数组构造出一个二叉堆

到此,本篇文章就讲解完了,其中介绍了二叉堆是什么,并以最大堆为例子,介绍了添加元素,删除最大元素的实现思路和代码实现,认真看完的小伙伴肯定有所收获