Min Heap 最小堆

最小堆能够动态地维护一组有序数据,使得最小的元素始终位于堆顶。堆也是优先队列的底层实现方式

什么是堆?

宏观来说, 堆能够动态地维护一组有序数据,使得最小或最大的元素始终位于堆顶. 对于最小堆来说, 最小的元素位于堆顶. 对于最大堆来说, 最大的元素位于堆顶

从数据结构角度来说, 堆是一个完全二叉树. 对于最小堆来说, 每一个父节点都比子节点元素要小. 对于最大堆

什么是完全二叉树?

完全二叉树就是所有节点按照从上到下, 从左到右的顺序排列,中间不能有空.

最小堆的定义:

最小堆是一个完全二叉树, 最小的元素位于堆顶, 父亲节点比孩子节点要小. 可以用下面这幅图来表示:

红色的节点为堆顶节点, 节点旁边的序号表示节点的序号.

很显然我们可以发现父节点和其字节点的索引规律:

  • 父节点= (子节点-1)/2

  • 左孩子=2*父节点+1

  • 右孩子=2*父节点+2

我们可以用一个数组将堆表示出来

索引

0

1

2

3

4

5

6

7

元素

6

7

12

9

8

13

14

10

实现最小堆

根据上面所说,我们可以用数组表示堆, 于是很轻易地写出最小堆的构造函数

public class MinHeap<E extends  Comparable<E>> {

    private Array<E> data;

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

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

我们用到了前面章节实现的 Array 动态数组作为堆的底层实现

一些简单的方法

我们实现了一些堆中常用的简单方法

  • getSize() 获取堆中元素个数

  • isEmpty判断堆是否为空

  • parent(index) 获取父节点的索引

  • leftChild(index) 获取左孩子的索引

  • rightChild(index)获取右孩子的索引

  • swap(int idx1, int idx) 交换两个元素

  • findMin()获得堆顶元素

  • isLeaf()判断某一个节点是否为叶子节点

public int getSize(){
    return data.getSize();
}

boolean isEmpty(){
    return data.isEmpty();
}

private int parent(int index){
    if (index==0){
        throw new IllegalArgumentException("index-0 doesn't have parent. ");
    }
    return (index-1)/2;
}

private int leftChild(int index){
    return index*2+1;
}

private int rightChild(int index){
    return index*2+2;
}

private  void swap(int idx1, int idx2){
    E temp=data.get(idx1);
    data.set(idx1,data.get(idx2));
    data.set(idx2,temp);
}

public E  findMin(){
    if (data.isEmpty()){
        throw new IllegalArgumentException("Cannot findMin when the heap is empty!");
    }
    return data.get(0);
}

//判断某个节点是否为叶子节点
public boolean isLeaf(int idx){
    //如果左孩子索引越界,说明没有左孩子,连左孩子都没有肯定是叶子节点
    return  leftChild(idx)< data.getSize();
}

shiftUp 和 shiftDown

shiftUp 与 shiftDown 是堆的核心操作, 堆的性质主要靠这两个操作进行维护.

先说 shiftUp 操作. shiftUp操作的原理非常简单, 将一个节点与父节点进行比较, 若比父节点小, 则与父节点交换. 如此循环, 直到到达根节点. 这个道理也非常简单, 就是保证父节点比孩子节点要小, 最小的元素一定要放在堆顶, 也就是根节点.

private void shiftUp(int idx){
    while (idx>0 ){//只要没有达到根节点, 就继续尝试与父节点交换

        //如果父节点更大, 就与父节点进行交换
        if (data.get(parent(idx)).compareTo(data.get(idx))>0){
            swap(idx,parent(idx));
            idx=parent(idx);
        }else {//否则, 跳出循环
            break;
        }
    }
}

再说shiftDown操作. shiftDown操作就是将一个元素不断地与左右孩子的较小节点进行比较, 若父节点更小, 则进行交换, 直到到达了叶子节点. 这个道理和shiftUp相同, 就是保证父节点比孩子节点要小, 最小的元素一定要放在堆顶, 也就是根节点.



private void shiftDown(int idx){

    while (isLeaf(idx)){//到了叶子节点就不用再shiftDown了,跳出循环

        int j=leftChild(idx);//j初始为左孩子索引
        if ((j+1)<data.getSize()){//说明有右孩子
            //而且右孩子比左孩子更小, 那就和右孩子交换
            if (data.get(j+1).compareTo(data.get(j))<0){
                j++;//j更新为右孩子索引
            }
        }

        //data[j]是idx节点左右孩子的最小值
        //如果 data[idx] 比 data[j] 还要小,就不需要向下交换了, 直接跳出循环
        if (data.get(idx).compareTo(data.get(j))<0){
            break;
        }
        //否则交换 data[idx] 和 data[j], 接着向下shiftdown
        swap(idx,j);
        idx=j;
    }
}

接口方法

有了上面的辅助方法, 我们可以来写一些常用的接口方法

add(E e) 向堆中添加元素

向堆中添加元素非常简单, 分为两步

  1. 直接向数组末尾添加新元素

  2. 对数组末尾元素进行 shiftUp 操作

用图表示如下

代码表示如下:

public void add(E e){
    data.addLast(e);
    shiftUp(data.getSize()-1);
}

extractMin() 取出堆顶元素

取出堆顶也元素非常简单, 分为4步

  1. 堆顶元素备份出来

  2. 用数组中最后一个元素替代堆顶元素

  3. 对新的堆顶元素进行shiftDown操作

  4. 返回原来的堆顶元素

用图表示如下

代码表示如下:

public E extractMin(){
    E ret=findMin();
    swap(0,data.getSize()-1);
    data.removeLast();
    shiftDown(0);
    return ret;
}

replace(E e) 取出堆顶元素并传入新的堆顶元素

代码逻辑和上面类似, 直接给出代码:

//取出最小值,并且替换成e
public E replace(E e){
    E ret=findMin();
    data.set(0,e);
    shiftDown(0);
    return ret;
}

heapify 操作

heapify 表示根据一个数组构造一个堆

我们先将数组元素提取出来, 再维护最小堆的性质(父节点始终比孩子节点要小). 为此, 我们需要从倒数第一个非叶子节点开始, 对每一个元素执行shiftDown 操作, 直到根节点. 这个道理也非常简单, 叶子节点是没有办法进行shiftDown操作的.

//heapify 操作, 传入一个数组, 根据这个数组构造出一个最小堆
public MinHeap(E[] arr){
    data=new Array(arr.length);
    for (int i = 0; i < arr.length; i++) {
        data.addLast(arr[i]);
    }
    for (int i = parent(arr.length-1); i >=0 ; i--) {
        shiftDown(i);
    }
}

至此, 我们完成了一个最小堆的所有操作

测试一下

我们用两个堆进行了测试, 一个是通过向堆中加入新元素再取出堆顶元素, 另一个是根据一个数组构造一个堆,再进行取出堆顶元素.

堆中取出的元素放入数组当中, 通过遍历数组, 判断数组是否有序来判断堆是否正确.

public static void main(String[] args) {
        int n=1000;

        Random random=new Random();

        //测试向空堆里面添加元素, 删除元素
        MinHeap<Integer> minHeap1=new MinHeap<>();

        for (int i = 0; i < n; i++) {
            minHeap1.add(random.nextInt(Integer.MAX_VALUE));
        }

        int[] arr1=new int[n];
        for (int i = 0; i < n; i++) {
            arr1[i]= minHeap1.extractMin();
        }

        for (int i = 1 ; i <n ; i++) {
            if (arr1[i-1]>arr1[i]){
                throw  new IllegalArgumentException("MinHeap erro !");
            }
        }

        //测试将一个数组构造成一个堆
        //随机数组
        Integer[] arr2=new Integer[n];
        for (int i = 0; i < n; i++) {
            arr2[i]=random.nextInt(Integer.MAX_VALUE);
        }

        //heapify操作
        MinHeap<Integer> minHeap2=new MinHeap<>(arr2);

        for (int i = 0; i < n; i++) {
            arr2[i]= minHeap2.extractMin();
        }

        for (int i = 1 ; i <n ; i++) {
            if (arr2[i-1]>arr2[i]){
                throw  new IllegalArgumentException("MinHeap erro !");
            }
        }


        System.out.println("Test MinHeap completed!");

    }



}

测试结果和我们预想的一下, 输出:

Test MinHeap completed!

Process finished with exit code 0

Last updated

Was this helpful?