数据结构—堆(Heap)的原理介绍以及Java代码的完全实现

  本文详细介绍了堆(Heap)这种数据结构的特点和原理,并且提供了Java代码的完全实现,包括大顶堆、小顶堆的构建,堆节点的添加、删除,大顶堆、小顶堆的排序等方法!

1 堆的概述

1.1 堆的概述

  要想了解堆,就必须了解二叉树的一些基本性质,如果不是很了解二叉树的,可以看看这篇文章:二叉树的入门以及Java实现案例详解
  这里所说的堆(Heap)是数据结构中的堆,而不是内存模型中的堆。堆是一种树形结构,它满足如下性质:

  1. 堆是一棵完全二叉树,也被称为二叉堆(binary heap),一般说的堆就是指二叉堆,实际上还有左倾堆、右倾堆等,它们不要求是完全二叉树。
  2. 堆中任意节点的值总是不大于/不小于其子节点的值;

  如果每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆/最大堆/大根堆,如下图左;或者每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆/最小堆/小根堆,如下图右。
在这里插入图片描述

1.2 堆的常见应用和实现

堆的应用:

  1. 堆常被用于实现“优先队列”(PriorityQueue)。优先队列可以自由添加数据,但取出数据时要从最小值开始按顺序取出;
  2. 堆还用于实现堆排序。

堆的实现:
  由于堆作为一颗完全二叉树,因此根据二叉树的性质,完全二叉树能够完美的映射到数组结构中去:如果节点从0开始编号,并把节点映射到数组中之后,则节点之间满足如下关系:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2](0<=i<=n/2 -1)
在这里插入图片描述
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2](0<=i<=n/2 -1)
在这里插入图片描述

  n为数组长度,n/2 -1实际上表示数组从头到尾最后一个非叶子结点的索引位置。
  因此,常常使用数组来实现堆结构,比如Java中的PriorityQueue,就是采用数组实现的二叉堆。由于堆算作一个偏序(只有父节点和子节点的大小关系,没有两个子节点之间的大小关系),因此同一批元素采用不同算法构建成的堆在数组中的实际存储顺序是不一定相同的,并且堆排序也是一种不稳定的排序算法

二叉堆和二叉排序树的区别:

  1. 在二叉排序树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。关于二叉排序树:二叉排序树的详解以及Java代码的完全实现
  2. 二叉排序树一般使用链表实现,占用的内存空间比它们存储的数据要多。必须为节点对象以及左/右子节点指针分配额外的内存。堆可以使用数组来存放数据,节点对象以及左/右子节点存在天然的关系,使用索引即可到达,节省内存。
  3. 由于二叉排序树中节点大小的性质,在二叉排序树中查找会很快,查找过程类似与有序数组的二分查找,并且查找次数不会超过树的深度,但是在堆中查找会比较慢。使用二叉排序树的目的是为了方便查找节点,使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关排序、插入、删除操作。

2 实现原理

  这里以大顶堆的构建为例子,该案例对应着下面实现代码中的案例

2.1 根据数组构建大顶堆

  这里的构建堆是从“父节点”入手进行构建。实际上是用到了堆排序的过程中的第一次构建大顶堆的过程,这里不多叙述,具体原理直接看这篇文章:10种常见排序算法原理详解以及Java代码的完全实现,可以直接看其中的堆排序的原理部分,讲的很详细。
  最终结果数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述

2.2 添加元素构建大顶堆

  这里的构建堆是从“新节点”入手进行构建,不同于堆排序的构建方式,这里详细解释一下。
  大概原理是: 每添加一个元素,则将其与父节点进行比较,如果新添加节点小于等于父节点,则添加元素到该位置;否则,继续向上寻找父节点,直到找到某个位置,使得位于该位置的新元素的值小于等于对应父节点的元素的值,并且将原位置上的元素一一向后挪动。这里比较抽象,我们看具体的过程。
  当添加第1个元素49时,strat=0,此时直接heap[0] = 49,即添加到堆的顶点。此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第2个元素38时,strat=1,parent=0,此时strat>0进入内层循环,判断父节点和子节点的大小,由于49>38,因此break跳出循环,此时直接heap[1] = 49此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第3个元素65时,strat=2,parent=0,此时strat>0进入内层循环,判断父节点和子节点的大小,由于49<65,此时将父节点的值移动到子节点位置处,heap[2] = heap[0],此时数组结构为{49, 38, 49},然后将start的索引值变成父节点的索引值,即start=0,重新计算parent=0。
  由于此时start=0,因此结束循环,此时直接heap[start] = 65,即heap[0]= 65。此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第4个元素97时,strat=3,parent=1,此时strat>0进入内层循环,判断父节点和子节点的大小,由于38<97,此时将父节点的值移动到子节点位置处,heap[3] = heap[1],此时数组结构为{65, 38, 49, 38},然后将start的索引值变成父节点的索引值,即start=1,重新计算parent=0。
  由于此时start>0开始第二次内层循环,判断父节点和子节点的大小,由于65<97,此时将父节点的值移动到子节点位置处,heap[1] = heap[0],此时数组结构为{65, 65, 49, 38},然后将start的索引值变成父节点的索引值,即start=0,重新计算parent=0。
  由于此时start=0,因此结束循环,此时直接heap[start] = 97,即heap[0]= 97。此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第5个元素76时,strat=4,parent=1,此时strat>0进入内层循环,判断父节点和子节点的大小,由于65<76,此时将父节点的值移动到子节点位置处,heap[4] = heap[1],此时数组结构为{97, 65, 49, 38, 65},然后将start的索引值变成父节点的索引值,即start=1,重新计算parent=0。
  由于此时start>0开始第二次内层循环,判断父节点和子节点的大小,由于76<97,因此break跳出循环,此时直接heap[1] = 76,此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第6个元素13时,strat=5,parent=2,此时strat>0进入内层循环,判断父节点和子节点的大小,由于13<49,因此break跳出循环,此时直接heap[5] = 13,此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第7个元素27时,strat=6,parent=2,此时strat>0进入内层循环,判断父节点和子节点的大小,由于27<49,因此break跳出循环,此时直接heap[6] = 27,此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第8个元素49时,strat=7,parent=3,此时strat>0进入内层循环,判断父节点和子节点的大小,由于49>38,此时将父节点的值移动到子节点位置处,heap[7] = heap[3],此时数组结构为{97, 76, 49, 38, 65, 13, 27, 38},然后将start的索引值变成父节点的索引值,即start=3,重新计算parent=1。
  由于此时start>0开始第二次内层循环,判断父节点和子节点的大小,由于49<76,因此break跳出循环,此时直接heap[3] = 49,此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  当添加第9个元素78时,strat=8,parent=3,此时strat>0进入内层循环,判断父节点和子节点的大小,由于78>49,此时将父节点的值移动到子节点位置处,heap[8] = heap[3],此时数组结构为{97, 76, 49, 49, 65, 13, 27, 38, 49},然后将start的索引值变成父节点的索引值,即start=3,重新计算parent=1。
  由于此时start>0开始第二次内层循环,判断父节点和子节点的大小,由于78>76,此时将父节点的值移动到子节点位置处,heap[3] = heap[1],此时数组结构为{97, 76, 49, 76, 65, 13, 27, 38, 49},然后将start的索引值变成父节点的索引值,即start=1,重新计算parent=0。
  由于此时start>0开始第三次内层循环,判断父节点和子节点的大小,由于78<97,因此break跳出循环,此时直接heap[1] = 78,此时数组中元素位置和映射到堆中的结构如下:
在这里插入图片描述
  这就是最终的构建结果,可以看到它和直接使用数组构建的最终结果序列是不一样的,但是他们都符合大顶堆的要求,这也证明了大顶堆的序列不唯一的性质。

2.3 删除元素

  1. 首先在数组中查找是否存在对应的元素,如果不存在则返回false;
  2. 如果存在则获取第一个找到的元素的索引(因为有可能有相同的元素,这里的删除是删除第一个找到的元素);
  3. 之后一步就和堆排序的过程中的重构大顶堆是一致的,实际上就是将需要删除的元素与堆尾元素互换,然后移除堆尾元素,之后从被删除元素的索引处开始向下重构大顶堆的过程。
  4. 向下重构大顶堆的操作只能保证从该索引位置开始下面的结构满足堆的要求。但是这里的向下重构和堆排序那里不同的是,堆排序是从堆顶部开始重构的,因此能够保证整个堆的满足要求,而这里却可能从堆的中间的某个结点开始的,在向下重构完毕之后,还需要校验起始索引位置的结点有没有调整位置,因为最开始就是一个大顶堆结构,即父结点大于等于子结点,如果调整了位置,那么说明起始索引之前的结构也满足要求;如果没有调整位置,那么可能出现起始位置的元素不仅大于等于其子结点,还可能大于其父结点的情况,此时还需要一次从该位置开始的向上的重构大顶堆来保证从该索引位置开始上面的结构也满足堆的要求!

2.4 堆排序

  这里不多叙述,具体原理直接看这篇文章:10种常见排序算法原理详解以及Java代码的完全实现,可以直接看其中的堆排序的原理部分,讲的很详细。

3 大顶堆的实现

  提供基于数组实现的大顶堆的类,提供根据集合构建大顶堆的方法,提供添加、删除节点的方法,提供了大顶堆的堆排序(顺序)的方法。

/**
 * 大顶堆的实现
 * {@link MaxBinaryHeap#MaxBinaryHeap()} 初始化空的大顶堆,使用默认容量
 * {@link MaxBinaryHeap#MaxBinaryHeap(int)} 初始化空的大顶堆,使用指定容量
 * {@link MaxBinaryHeap#MaxBinaryHeap(Comparator)} 初始化空的大顶堆,使用指定比较器
 * {@link MaxBinaryHeap#MaxBinaryHeap(int, Comparator)} 初始化空的大顶堆,使用指定容量和比较器
 * {@link MaxBinaryHeap#MaxBinaryHeap(Collection)} 根据指定集合元素构建大顶堆
 * {@link MaxBinaryHeap#MaxBinaryHeap(Collection, Comparator)} 根据指定集合元素构建大顶堆,使用自定义比较器
 * {@link MaxBinaryHeap#add(Object)} 添加元素,并重构大顶堆
 * {@link MaxBinaryHeap#remove(Object)} 删除元素,并重构大顶堆
 * {@link MaxBinaryHeap#heapSort()} 大顶堆排序(顺序)
 * {@link MaxBinaryHeap#toString()} 输出大顶堆
 *
 * @author lx
 */
public class MaxBinaryHeap<E> {
    /**
     * 堆的物理存储结构,即使用数组来实现
     */
    private Object[] heap;

    /**
     * 节点数量
     */
    private int size;

    /**
     * 容量
     */
    private static int capacity = 16;

    /**
     * 如果元素使用自然排序,那么比较器为null;否则使用比较器比较
     */
    private final Comparator<? super E> cmp;


    /**
     * 对元素进行比较大小的方法,如果传递了自定义比较器,则使用自定义比较器,否则则需要数据类型实现Comparable接口
     *
     * @param e1 被比较的第一个对象
     * @param e2 被比较的第二个对象
     * @return 0 相等 ;小于0 e1 < e2 ;大于0 e1 > e2
     */
    private int compare(E e1, E e2) {
        if (cmp != null) {
            return cmp.compare(e1, e2);
        } else {
            return ((Comparable<E>) e1).compareTo(e2);
        }
    }


    /**
     * 初始化空的大顶堆,使用默认容量
     */
    public MaxBinaryHeap() {
        this(capacity, null);
    }

    /**
     * 初始化空的大顶堆,指定容量
     *
     * @param initCapacity 指定容量数组
     */
    public MaxBinaryHeap(int initCapacity) {
        this(initCapacity, null);
    }

    /**
     * 初始化空的大顶堆,指定比较器
     *
     * @param comparator 指定比较器
     */
    public MaxBinaryHeap(Comparator<? super E> comparator) {
        this(capacity, comparator);
    }

    /**
     * 初始化空的大顶堆,指定容量和比较器
     *
     * @param initCapacity 指定数组容量
     * @param comparator   指定比较器
     */
    public MaxBinaryHeap(int initCapacity, Comparator<? super E> comparator) {
        if (initCapacity < 1) {
            throw new IllegalArgumentException();
        }
        capacity = initCapacity;
        this.heap = new Object[initCapacity];
        cmp = comparator;
    }

    /**
     * 同通过一批数据初始化大顶堆
     *
     * @param heap 数组
     */
    public MaxBinaryHeap(Collection<? extends E> heap) {
        this(heap, null);
    }


    /**
     * 同通过一批数据和指定比较器初始化大顶堆
     *
     * @param heap       数组
     * @param comparator 自定义的比较器
     */
    public MaxBinaryHeap(Collection<? extends E> heap, Comparator<? super E> comparator) {
        Object[] array = heap.toArray();
        this.cmp = comparator;
        if (array.getClass() != Object[].class) {
            array = Arrays.copyOf(array, array.length, Object[].class);
        }
        for (Object o : array) {
            if (o == null) {
                throw new NullPointerException();
            }
        }
        this.heap = array;
        this.size = array.length;
        buildHeap(this.heap);
    }

    /**
     * 同通过一批数据初始化大顶堆
     *
     * @param heap 数据数组
     */
    private void buildHeap(Object[] heap) {
        /*i从最后一个非叶子节点的索引开始,递减构建,直到i=-1结束循环
        这里元素的索引是从0开始的,所以最后一个非叶子节点array.length/2 - 1,这是利用了完全二叉树的性质*/
        for (int i = heap.length / 2 - 1; i >= 0; i--) {
            buildHeap(heap, i, heap.length);
        }
    }

    /**
     * 同通过一批数初始化大顶堆
     *
     * @param arr    数据数组
     * @param i      非叶子节点的索引
     * @param length 堆长度
     */
    private void buildHeap(Object[] arr, int i, int length) {
        //先把当前非叶子节点元素取出来,因为当前元素可能要一直移动
        Object temp;
        //节点的子节点的索引
        int childIndex;
        /*循环判断父节点是否大于两个子节点,如果左子节点索引大于等于堆长度 或者父节点大于两个子节点 则结束循环*/
        for (temp = arr[i]; (childIndex = 2 * i + 1) < length; i = childIndex) {
            //childIndex + 1 < length 说明该节点具有右子节点,并且如果如果右子节点的值大于左子节点,那么childIndex自增1,即childIndex指向右子节点索引
            if (childIndex + 1 < length && compare((E) arr[childIndex], (E) arr[childIndex + 1]) < 0) {
                childIndex++;
            }
            //如果发现最大子节点(左、右子节点)大于根节点,为了满足大顶堆根节点的值大于子节点,需要进行值的交换
            //如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,交换之后继续循环对子节点所在的树进行判断
            if (compare((E) arr[childIndex], (E) temp) > 0) {
                swap(arr, i, childIndex);
            } else {
                //走到这里,说明父节点大于最大的子节点,满足大顶堆的条件,直接终止循环
                break;
            }
        }
    }


    /**
     * 大顶堆排序(顺序)
     * 实际上就是不断循环将堆顶元素与堆尾元素互换,然后移除堆尾元素,之后重构大顶堆的过程
     */
    public Object[] heapSort() {
        //使用大顶堆的副本进行排序输出
        Object[] arr = Arrays.copyOf(heap, size);
        /*开始堆排序,i = arr.length - 1,即从大顶堆尾部的数开始,直到i=0结束循环*/
        for (int i = size - 1; i > 0; i--) {
            //交换堆顶与堆尾元素顺序
            swap(arr, 0, i);
            //重新构建大顶堆
            buildHeap(arr, 0, i);
        }
        return arr;
    }


    /**
     * 添加节点,构建大顶堆
     *
     * @param e 需要添加的节点
     */
    public void add(E e) {
        /*判空*/
        if (e == null) {
            throw new NullPointerException();
        }
        /*检查容量*/
        if (heap.length == size) {
            resize();
        }
        /*添加节点*/
        addNode(e, size++);
    }


    /**
     * 添加节点,并向上重构大顶堆,最终找到一个位置加入新结点e,该位置的结点小于等于其父结点
     *
     * @param e 要添加的节点
     */
    private void addNode(E e, int start) {

        //获取size处节点的父节点索引
        int parent = (start - 1) / 2;
        /*如果size>0 寻找合适的位置:在某个插入的位置的新节点小于等于对应的父节点的值*/
        while (start > 0) {
            //判断父节点和新子节点的大小,如果父节点小于等于新子节点,那么符合小顶堆的要求,重构结束,该start就是子节点插入的位置
            if (compare((E) heap[parent], e) >= 0) {
                break;
            } else {
                //否则,将父节点的值移动到子节点的位置处
                heap[start] = heap[parent];
                //将start的索引值变成父节点的索引值
                start = parent;
                //重新计算父节点的索引,不断循环,直到找到父节点值小于等于新子节点值的索引
                parent = (start - 1) / 2;
            }
        }
        //在合适的位置插入新节点值
        heap[start] = e;
    }


    /**
     * 底层数组扩容
     */
    private void resize() {
        heap = Arrays.copyOf(heap, heap.length * 2, Object[].class);
    }

    /**
     * 交换元素
     *
     * @param arr 数组
     * @param a   元素的下标
     * @param b   元素的下标
     */
    private static void swap(Object[] arr, int a, int b) {
        Object temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }


    /**
     * 删除找到的第一个堆节点,并且重构大顶堆
     *
     * @param e 需要删除的节点
     * @return false 删除失败 true 删除成功
     */
    public boolean remove(E e) {
        int eIndex = -1;
        for (int i = 0; i < size; i++) {
            //这里是通过compare来查找元素是否相同的
            if (compare((E) heap[i], e) == 0) {
                eIndex = i;
            }
        }
        /*没找到需要删除的元素*/
        if (eIndex == -1) {
            return false;
        }
        /*找到了*/
        //原尾部元素x
        E x = (E) heap[size - 1];
        //交换查找到的元素与堆尾元素的位置
        swap(heap, eIndex, size - 1);
        //移除堆尾元素
        heap[size--] = null;
        //从eIndex开始向下重新构建大顶堆
        buildHeap(heap, eIndex, size);
        //构建之后如果eIndex位置的元素就是x,说明没有调整堆结构,那么将该位置的元素看成新插入的元素,需要向上构建大顶堆
        if (heap[eIndex] == x) {
            //调用addNode从eIndex开始向上重构大顶堆
            addNode(x, eIndex);
        }
        return true;
    }

    public int size(){
        return size;
    }


    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[");
        for (int i = 0; i < size; i++) {
            stringBuilder.append(heap[i]);
            if (i != size - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("]");
        return stringBuilder.toString();
    }
}

4 小顶堆的实现

  提供基于数组实现的小顶堆的类,提供根据集合构建小顶堆的方法,提供添加、删除节点的方法,提供了小顶堆的堆排序(逆序)的方法。实际上,大顶堆和小顶堆的实现的区别仅仅在于某些比较条件是相反,大部分代码都是相同的。

/**
 * 小顶堆的实现
 * {@link MinBinaryHeap#MinBinaryHeap()} 初始化空的小顶堆,使用默认容量
 * {@link MinBinaryHeap#MinBinaryHeap(int)} 初始化空的小顶堆,使用指定容量
 * {@link MinBinaryHeap#MinBinaryHeap(Comparator)} 初始化空的小顶堆,使用指定比较器
 * {@link MinBinaryHeap#MinBinaryHeap(int, Comparator)} 初始化空的小顶堆,使用指定容量和比较器
 * {@link MinBinaryHeap#MinBinaryHeap(Collection)} 根据指定集合元素构建小顶堆
 * {@link MinBinaryHeap#MinBinaryHeap(Collection, Comparator)} 根据指定集合元素构建小顶堆,使用自定义比较器
 * {@link MinBinaryHeap#add(Object)} 添加元素,并重构小顶堆
 * {@link MinBinaryHeap#remove(Object)} 删除元素,并重构小顶堆
 * {@link MinBinaryHeap#heapSort()} 小顶堆排序(顺序)
 * {@link MinBinaryHeap#toString()} 输出小顶堆
 *
 * @author lx
 */
public class MinBinaryHeap<E> {
    /**
     * 堆的物理结构,即使用数组来实现
     */
    private Object[] heap;

    /**
     * 节点数量
     */
    private int size;

    /**
     * 容量
     */
    private static int capacity = 16;

    /**
     * 如果元素使用自然排序,那么比较器为null
     */
    private final Comparator<? super E> cmp;


    /**
     * 对元素进行比较大小的方法,如果传递了自定义比较器,则使用自定义比较器,否则则需要数据类型实现Comparable接口
     *
     * @param e1 被比较的第一个对象
     * @param e2 被比较的第二个对象
     * @return 0 相等 ;小于0 e1 < e2 ;大于0 e1 > e2
     */
    private int compare(E e1, E e2) {
        if (cmp != null) {
            return cmp.compare(e1, e2);
        } else {
            return ((Comparable<E>) e1).compareTo(e2);
        }
    }


    /**
     * 初始化空的小顶堆,使用默认容量
     */
    public MinBinaryHeap() {
        this(capacity, null);
    }

    /**
     * 初始化空的小顶堆,指定容量
     *
     * @param initCapacity 指定容量数组
     */
    public MinBinaryHeap(int initCapacity) {
        this(initCapacity, null);
    }

    /**
     * 初始化空的小顶堆,指定比较器
     *
     * @param comparator 指定比较器
     */
    public MinBinaryHeap(Comparator<? super E> comparator) {
        this(capacity, comparator);
    }

    /**
     * 初始化空的小顶堆,指定容量和比较器
     *
     * @param initCapacity 指定数组容量
     * @param comparator   指定比较器
     */
    public MinBinaryHeap(int initCapacity, Comparator<? super E> comparator) {
        if (initCapacity < 1) {
            throw new IllegalArgumentException();
        }
        capacity = initCapacity;
        this.heap = new Object[initCapacity];
        cmp = comparator;
    }

    /**
     * 同通过一批数据初始化小顶堆
     *
     * @param heap 数组
     */
    public MinBinaryHeap(Collection<? extends E> heap) {
        this(heap, null);
    }


    /**
     * 同通过一批数据和指定比较器初始化小顶堆
     *
     * @param heap       数组
     * @param comparator 自定义的比较器
     */
    public MinBinaryHeap(Collection<? extends E> heap, Comparator<? super E> comparator) {
        Object[] array = heap.toArray();
        this.cmp = comparator;
        if (array.getClass() != Object[].class) {
            array = Arrays.copyOf(array, array.length, Object[].class);
        }
        for (Object o : array) {
            if (o == null) {
                throw new NullPointerException();
            }
        }
        this.heap = array;
        this.size = array.length;
        buildHeap(this.heap);
    }

    /**
     * 构建小顶堆
     *
     * @param heap 一批数据
     */
    private void buildHeap(Object[] heap) {
        /*i从最后一个非叶子节点的索引开始,递减构建,直到i=-1结束循环
        这里元素的索引是从0开始的,所以最后一个非叶子节点array.length/2 - 1,这是利用了完全二叉树的性质*/
        for (int i = heap.length / 2 - 1; i >= 0; i--) {
            buildHeap(heap, i, heap.length);
        }
    }


    /**
     * 从指定索引向下构建小顶堆,最终该位置的结点小于等于其子结点
     *
     * @param arr    数组
     * @param i      非叶子节点的索引
     * @param length 堆长度
     */
    private void buildHeap(Object[] arr, int i, int length) {
        //先把当前非叶子节点元素取出来,因为当前元素可能要一直移动
        Object temp;
        //节点的子节点的索引
        int childIndex;
        /*循环判断父节点是否大于两个子节点,如果左子节点索引大于等于堆长度 或者父节点大于两个子节点 则结束循环*/
        for (temp = arr[i]; (childIndex = 2 * i + 1) < length; i = childIndex) {
            //childIndex + 1 < length 说明该节点具有右子节点,并且如果如果右子节点的值小于左子节点,那么childIndex自增1,即childIndex指向右子节点索引
            if (childIndex + 1 < length && compare((E) arr[childIndex], (E) arr[childIndex + 1]) > 0) {
                childIndex++;
            }
            //如果发现最小子节点(左、右子节点)小于根节点,为了满足小顶堆根节点的值小于子节点,需要进行值的交换
            //如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,交换之后继续循环对子节点所在的树进行判断
            if (compare((E) arr[childIndex], (E) temp) < 0) {
                swap(arr, i, childIndex);
            } else {
                //走到这里,说明父节点小于等于最小的子节点,满足小顶堆的条件,直接终止循环
                break;
            }
        }
    }

    /**
     * 小顶堆排序(顺序)
     *
     * @param arr 需要被排序的数据集合
     */
    public void bigHeapSort(Object[] arr) {
        /*1、构建小顶堆*/
        /*i从最后一个非叶子节点的索引开始,递减构建,直到i=-1结束循环
        这里元素的索引是从0开始的,所以最后一个非叶子节点array.length/2 - 1,这是利用了完全二叉树的性质*/
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            buildHeap(arr, i, arr.length);
        }
        /*2、开始堆排序,i = arr.length - 1,即从小顶堆尾部的数开始,直到i=0结束循环*/
        for (int i = arr.length - 1; i > 0; i--) {
            //交换堆顶与堆尾元素顺序
            swap(arr, 0, i);
            //重新构建小顶堆
            buildHeap(arr, 0, i);
        }
    }


    /**
     * 小顶堆排序(逆序)
     * 实际上就是不断循环将堆顶元素与堆尾元素互换,然后移除堆尾元素,之后重构小顶堆的过程
     */
    public Object[] heapSort() {
        //使用小顶堆的副本进行排序输出
        Object[] arr = Arrays.copyOf(heap, size);
        /*2、开始堆排序,i = arr.length - 1,即从小顶堆尾部的数开始,直到i=0结束循环*/
        for (int i = arr.length - 1; i > 0; i--) {
            //交换堆顶与堆尾元素顺序
            swap(arr, 0, i);
            //重新构建小顶堆,此时堆的大小为交换前堆大小-1
            buildHeap(arr, 0, i);
        }
        return arr;
    }

    /**
     * 添加节点
     *
     * @param e 被添加的节点元素
     */
    public void add(E e) {
        /*判空*/
        if (e == null) {
            throw new NullPointerException();
        }
        /*检查容量*/
        if (heap.length == size) {
            resize();
        }
        /*添加节点*/
        addNode(e, size++);
    }

    /**
     * 添加节点,并向上重构小顶堆,最终找到一个位置加入新结点e,该位置的结点大于等于其父结点
     *
     * @param e     要添加的节点
     * @param start 即新添加元素所在的索引
     */
    private void addNode(E e, int start) {
        //获取size处结点的父节点索引
        int parent = (start - 1) / 2;
        /*如果size>0 寻找合适的位置:在某个插入的位置的新节点大于等于对应的父节点的值*/
        while (start > 0) {
            //判断父节点和新子节点的大小,如果父节点小于等于新子节点,那么符合小顶堆的要求,重构结束
            if (compare((E) heap[parent], e) <= 0) {
                break;
            } else {
                //否则,将父节点的值移动到子节点的位置处
                heap[start] = heap[parent];
                //将start的索引值变成父节点的索引值
                start = parent;
                //重新计算父节点的索引,不断循环,直到找到父节点值小于等于新子节点值的索引
                parent = (start - 1) / 2;
            }
        }
        //在合适的位置插入新节点值
        heap[start] = e;
    }

    /**
     * 扩容
     */
    private void resize() {
        heap = Arrays.copyOf(heap, heap.length * 2, Object[].class);
    }


    /**
     * 交换元素
     *
     * @param arr 数组
     * @param a   元素的下标
     * @param b   元素的下标
     */
    private static void swap(Object[] arr, int a, int b) {
        Object temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }


    /**
     * 删除找到的第一个堆节点,并且重构小顶堆
     *
     * @param e 需要删除的节点
     * @return false 删除失败 true 删除成功
     */
    public boolean remove(E e) {
        int eIndex = -1;
        for (int i = 0; i < size; i++) {
            if (compare((E) heap[i], e) == 0) {
                eIndex = i;
            }
        }
        if (eIndex == -1) {
            return false;
        }
        //原尾部元素x
        E x = (E) heap[size - 1];
        //交换查找到的元素与堆尾元素的位置
        swap(heap, eIndex, size - 1);
        //移除堆尾元素
        heap[size--] = null;
        //从eIndex开始向下重新构建小顶堆
        buildHeap(heap, eIndex, size);
        //构建之后如果eIndex位置的元素就是x,说明没有调整堆结构,那么将该位置的元素看成新插入的元素,需要向上构建小顶堆
        if (heap[eIndex] == x) {
            //调用addNode从eIndex开始向上重构小顶堆
            addNode(x, eIndex);
        }
        return true;
    }


    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[");
        for (int i = 0; i < size; i++) {
            stringBuilder.append(heap[i]);
            if (i != size - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("]");
        return stringBuilder.toString();
    }
}

5 测试案例

/**
 * 堆测试
 *
 * @author lx
 */
public class BinaryTest {
    /**
     * 通过数组在构造器中构建大顶堆
     */
    @Test
    public void testMaxBinaryHeap1() {
        Integer[] arr = new Integer[]{49, 38, 65, 97, 76, 13, 27, 49, 78};
        //构建大顶堆
        MaxBinaryHeap<Integer> maxBinaryHeap = new MaxBinaryHeap<>(Arrays.asList(arr));
        //输出大顶堆
        System.out.println(maxBinaryHeap);

        //添加节点,并且重构大顶堆
        maxBinaryHeap.add(11);
        maxBinaryHeap.add(77);
        //输出大顶堆
        System.out.println(maxBinaryHeap);

        //删除节点,并且重构大顶堆
        //删除失败
        System.out.println(maxBinaryHeap.remove(79));
        //删除成功
        System.out.println(maxBinaryHeap.remove(78));
        //输出大顶堆
        System.out.println(maxBinaryHeap);

        //大顶堆排序(顺序排序)
        System.out.println(Arrays.toString(maxBinaryHeap.heapSort()));
        //输出大顶堆
        System.out.println(maxBinaryHeap);
    }

    /**
     * 通过add方法构建大顶堆
     */
    @Test
    public void testMaxBinaryHeap2() {
        MaxBinaryHeap<Integer> maxBinaryHeap = new MaxBinaryHeap<>();
        maxBinaryHeap.add(49);
        maxBinaryHeap.add(38);
        maxBinaryHeap.add(65);
        maxBinaryHeap.add(97);
        maxBinaryHeap.add(76);
        maxBinaryHeap.add(13);
        maxBinaryHeap.add(27);
        maxBinaryHeap.add(49);
        maxBinaryHeap.add(78);
        //输出大顶堆  [97,78,49,76,65,13,27,38,49]
        System.out.println(maxBinaryHeap);

        //添加节点,并且重构大顶堆
        maxBinaryHeap.add(11);
        maxBinaryHeap.add(77);
        //输出大顶堆
        System.out.println(maxBinaryHeap);

        //删除节点,你
        //删除失败
        System.out.println(maxBinaryHeap.remove(79));
        //删除成功
        System.out.println(maxBinaryHeap.remove(78));
        //输出大顶堆
        System.out.println(maxBinaryHeap);

        //大顶堆排序(顺序排序)
        System.out.println(Arrays.toString(maxBinaryHeap.heapSort()));
        //输出大顶堆
        System.out.println(maxBinaryHeap);
    }

    /**
     * 通过数组在构造器中构建小顶堆
     */
    @Test
    public void testMinBinaryHeap1() {
        Integer[] arr = new Integer[]{49, 38, 65, 97, 76, 13, 27, 49, 78};
        //构建小顶堆
        MinBinaryHeap<Integer> minBinaryHeap = new MinBinaryHeap<>(Arrays.asList(arr));
        //输出小顶堆
        System.out.println(minBinaryHeap);

        //添加节点,并且重构小顶堆
        minBinaryHeap.add(11);
        minBinaryHeap.add(77);
        //输出小顶堆
        System.out.println(minBinaryHeap);

        //删除节点,并且重构小顶堆
        //删除失败
        System.out.println(minBinaryHeap.remove(79));
        //删除成功
        System.out.println(minBinaryHeap.remove(78));
        //输出小顶堆
        System.out.println(minBinaryHeap);

        //小顶堆排序(逆序排序)
        System.out.println(Arrays.toString(minBinaryHeap.heapSort()));
        //输出小顶堆
        System.out.println(minBinaryHeap);
    }

    /**
     * 通过add方法构建小顶堆
     */
    @Test
    public void testMinBinaryHeap2() {
        MinBinaryHeap<Integer> minBinaryHeap = new MinBinaryHeap<>();
        minBinaryHeap.add(49);
        minBinaryHeap.add(38);
        minBinaryHeap.add(65);
        minBinaryHeap.add(97);
        minBinaryHeap.add(76);
        minBinaryHeap.add(13);
        minBinaryHeap.add(27);
        minBinaryHeap.add(49);
        minBinaryHeap.add(78);
        //输出小顶堆
        System.out.println(minBinaryHeap);

        //添加节点,并且重构小顶堆
        minBinaryHeap.add(11);
        minBinaryHeap.add(77);
        //输出小顶堆
        System.out.println(minBinaryHeap);

        //删除节点,并且重构小顶堆
        //删除失败
        System.out.println(minBinaryHeap.remove(79));
        //删除成功
        System.out.println(minBinaryHeap.remove(78));
        //输出小顶堆
        System.out.println(minBinaryHeap);

        //小顶堆排序(逆序排序)
        System.out.println(Arrays.toString(minBinaryHeap.heapSort()));
        //输出小顶堆
        System.out.println(minBinaryHeap);
    }


    /**
     * remove交换节点之后发生的节点小于等于子节点 但是也同时小于父结点的情况
     */
    @Test
    public void testMinBinaryHeap3() {
        MinBinaryHeap<Integer> minBinaryHeap = new MinBinaryHeap<>();
        //添加节点,并且重构小顶堆
        minBinaryHeap.add(0);
        minBinaryHeap.add(25);
        minBinaryHeap.add(1);
        minBinaryHeap.add(30);
        minBinaryHeap.add(35);
        minBinaryHeap.add(7);
        minBinaryHeap.add(3);
        minBinaryHeap.add(40);
        minBinaryHeap.add(45);
        minBinaryHeap.add(50);
        minBinaryHeap.add(55);
        minBinaryHeap.add(8);
        minBinaryHeap.add(9);
        minBinaryHeap.add(15);
        minBinaryHeap.add(16);
        System.out.println(minBinaryHeap);
        System.out.println(Arrays.toString(minBinaryHeap.heapSort()));
        //移除30之后,30和16交换位置,此时16小于等于40 45是成立的,因此没有调整位置
        //但是16却也小于父结点25,因此需要向上构建一次构建
        minBinaryHeap.remove(30);
        System.out.println(minBinaryHeap);
        System.out.println(Arrays.toString(minBinaryHeap.heapSort()));
    }

参考
  《数据结构与算法》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/106225629