最小堆的实现

最小堆的实现

最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。 ----百度百科。

我用俗话说吧。

  1. 数组来实现二叉树,所以满足二叉树的特性。
  2. 根元素是最小的元素,父节点小于它的两个子节点。
  3. 树中的元素是相对有序的。

如何实现堆的相对有序是关键。

插入元素时,插入到数组中的最后一个元素的后面,然后与该节点的父节点比较大小。如果插入的元素小于父节点元素,那么与父节点交换位置。然后插入元素交换到父节点位置时,又与该节点的父节点比较,直到大于父节点元素或者到达堆顶。该过程叫做上浮,即插入时上浮。

移除元素时,只能从堆顶移除元素,再取最后一个元素放到堆顶中。然后堆顶节点与子节点比较时,先取子节点中的较小者,如果堆顶节点大于较小子节点,那么交换位置。此时堆顶节点元素交换到较小子节点上。然后再与其较小子节点比较,直到小于较小子节点或者到达叶子节点为止。该过程叫做下沉,即移除元素时下沉。

先看数组元素对应的父子关系。先感受感受灵魂画法。

图一: 未使用index = 0元素的数组
图二: 使用index=0元素的数组

上面两个在数据结构上没什么区别,但是在实现上有所差别。


父子节点的下标关系

  1. 未使用index = 0元素的数组的父子节点的下标关系,即图一:

k=父节点的index -> 左子节点的index = 2k, 右子节点的index = 2k + 1

j = 子节点的index -> 父节点的index = j / 2


2. 使用index = 0元素的数组的父子节点的下标关系,即图二:

k=父节点的index -> 左子节点的index = 2k + 1, 右子节点的index = (k + 1) x 2

j = 子节点的index -> 父节点的index = (j -1) / 2


父子节点的下标关系是实现上浮和下沉的关键,选取节点的判断逻辑就是依靠上面的下标关系的。


下面来分析上浮和下沉的过程。

最小堆使用的数组,但是表现出来的数据结构是二叉树,所以上浮和下沉用树出来。 各位,原谅我画图太烂, 网上找了一下,没找到适合的画图工具,麻烦各位推荐一下。所以下面又要启动我的终极技能, 灵魂画术。

先分析上浮过程:

  1. 上面这个最小堆上插入一个元素,值= 2。放入最后一个元素中。

2. 与父节点的值比较,此处父节点的值=4,即插入的值小于父节点的值(2 < 4),那么交换。交换后如下图所示。

3. 交换之后,又与该节点的父节点进行比较,此处父节点的值=3, 即插入的值还是小于父节点的值(2 < 3),那么交换,交换后如下图所示。

4. 交换之后,还是与该节点的父节点进行比较,此处父节点的值=1, 即插入的值大于父节点的值(2 >1),那么上浮结束。插入元素已找到适合它的位置。


接着分析下沉过程。还是拿这份数据来分析。

图11: 移除堆顶元素

下沉发生在移除元素时,先移除 值=1的堆顶元素。如图11所示。

图12:将最后一个元素提到堆顶

将最后一个 值= 4的元素提到堆顶。如图12所示。

再与子节点中的较小者比较。


图13: 与较小子节点交换后的结构

子节点的较小者是 值=2的左子节点。此处值 = 4的堆顶元素(父节点) > 值=2的左子节点,则发生交换。此时 值=4的元素到达左子节点中。

然后再与其子节点中的较小者比较,此处的较小者子节点恰好又是左子节点。

此处 值 = 4的父节点 > 值 = 3的左子节点, 则发生交换。此时 值 = 4的元素又到达左子节点中。

然后 值=4的节点,再次与子节点中的较小者比较,此处因为只有一个子节点了,且父节点的值 < 子节点。所以不发生交换。则此时值=4的元素已经到达适合它的节点。

至此上浮和下沉已经分析完了。接下来就动手写一个最小堆。

/**
 * 最小堆的超简化版实现  
 * Created by wang007 on 2018/6/12.
 */
public class MinHeap {

    private int[] values = new int[16] ;

    private int size;


    /**
     * 移除并获取一个堆顶元素
     * @return 堆顶元素
     */
    public int poll() {
        if(size <= 0) throw new IllegalStateException("不存在元素");
        int value = values[0];
        values[0] = values[--size] ; //将最后一个元素提到堆顶
        values[size] = 0 ; //清空最后一个的数据
        fixDown(); //下沉操作
        return value;
    }


    /**
     * 下沉
     * @return 下沉到适合位置的index
     */
    private int fixDown() {
        int f = 0 ; //父节点的index
        int k ;  //较小者子节点的index
        while((k = (f << 1) + 1) < size) { //至少存在左子节点
            if(k < size - 1) {   //存在右子节点
                if (values[k] > values[k + 1]) k++; //左右子节点进行比较。
            }
            if(values[f] <= values[k]) break; //父节点小于较小者子节点,则找到合适的位置,退出循环
            int temp = values[f] ; values[f] = values[k]; values[k] = temp ;
            f = k ;
        }
        return f;
    }

    /**
     * 上浮
     * @return
     */
    private int fixUp() {
        int  j = size -1 ;   //最后一个元素的下标
        int f ; //父节点的下标
        while((f = ((j -1) >>1)) >= 0) { //通过父节点的下标
            if(values[f] <= values[j]) break; //父节点的值小于子节点的值,则打适合的位置。
            int temp = values[f] ; values[f] = values[j]; values[j] = temp ;
            j = f ;
        }
        return f;
    }

    /**
     * 添加一个元素在最小堆中上
     * @return
     */
    public int push(int item) {
        if(size >= values.length) Arrays.copyOf(values, size << 1) ;
        values[size++] = item ;
        return fixUp();
    }

    public static void main(String[] args) {
        MinHeap heap = new MinHeap();
        heap.push(4);
        heap.push(2);
        heap.push(7);
        heap.push(9);
        heap.push(1);
        heap.push(5);
        heap.push(10);
        heap.push(3);
        heap.push(2);
        for (int i = 0 ;i< 9; i++) {
            System.out.println(heap.poll());
        }
    }
}

为什么要讲这个最小堆呢? 因为定时任务的延迟队列就是使用最小堆实现的。 例如Timer,

ScheduleExecutorService

各位看官,看到这些可爱的图,难道就不想点个赞再走嘛? ~~

编辑于 2019-05-28 23:42