数据结构系列——堆

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。

堆的性质

堆的实现通过构造二叉堆(binary heap),实为二叉树的一种;由于其应用的普遍性,当不加限定时,均指该数据结构的这种实现。这种数据结构具有以下性质:

  • 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
    将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

堆的主要应用场景有:堆排序以及优先队列

  • 堆排序可具体参考排序算法系列——堆排序
  • 优先队列可参考JDK并发工具类源码学习系列——PriorityBlockingQueue
堆的表现形式

对于理解堆以及实现堆很重要的一点就是明白堆的表现形式,堆是树的一种,所以很自然的想到使用链表来实现堆,其实不然,由于我们需要频繁的对堆进行增加删除,所以一般堆的底层都是通过数组来实现,那么就有一个问题:数组如何表现出堆的结构呢?这里就有一个规则,即数组的第一个元素(即下标为0的元素)为堆的根节点,其他节点按照堆结构自上而下,自左而右依次表示为数组中的元素,这是一种既非前序也非后序,更非中序的遍历树的方式。具体见下图。

明白了堆如何使用数组来表述,那么我们就很容易理解下面堆的结构特征了。

  • parent(t)=(t - 1) >> 1,即t的父节点的下标=(t-1)/2,注意此处是整除,例如:t = 6,parent(t) = 2
  • left(t)=t << 1 + 1,即t的左孩子的节点下标=t * 2 + 1,例如:t = 2,left(t) = 5
  • right(t)=t << 1 + 2,即t的右孩子的节点下标=t * 2 + 2,例如:t = 2, right(t) = 6
  • 说明:此处的下标是从0开始
堆的操作

针对堆主要的操作有:

  • 构建堆:将一个数组构建成堆的结构
  • 移除堆的根节点:从堆中移除最大值(大顶堆)或者最小值(小顶堆)
  • 往堆中插入一个节点:插入节点
  • 调整堆:在对堆的结构进行修改之后需要进行的操作,以保证堆结构
堆的具体实现

下面看看一个Java版的堆的实现。

package com.vicky.datastructure.tree.heap;

import java.util.Arrays;

/** * * <p> * 数据结构——树——堆 * </p> * * @author Vicky * @email [email protected] * @2015年11月18日 * */
public abstract class Heap {
    protected int[] data;
    protected int length = 0;

    public Heap(int[] data) {
        this.data = data;
        this.length = data.length;
    }

    /** * 构建堆 */
    public abstract Heap buildHeap();

    /** * 删除一个节点,只能删除根节点 * * @return */
    public abstract Heap remove();

    /** * 插入一个节点,只能插入到最后 * * @param value * @return */
    public abstract Heap insert(int value);

    /** * 从node开始自上而下调整堆 * * @param node */
    public abstract void adjustDownHeap(int node);

    /** * 从node开始自下而上调整堆 * * @param node */
    public abstract void adjustUpHeap(int node);

    /** * 交换元素 * * @param n1 * @param n2 */
    public void swap(int n1, int n2) {
        int temp = data[n1];
        data[n1] = data[n2];
        data[n2] = temp;
    }

    /** * 获取node的父节点的索引 * * @param node * @return */
    protected int getParentIndex(int node) {
        return (node - 1) >> 1;
    }

    /** * 获取node的右孩子索引 * * @param node * @return */
    protected int getRightChildIndex(int node) {
        return (node << 1) + 1;
    }

    /** * 获取node的左孩子索引 * * @param node * @return */
    protected int getLeftChildIndex(int node) {
        return (node << 1) + 2;
    }

    /** * 打印堆 */
    protected void print() {
        System.out.println(Arrays.toString(data));
    }
}

package com.vicky.datastructure.tree.heap;

import java.util.Arrays;

/** * <p> * 最大堆 * </p> * * @author Vicky * @email [email protected] * @2015年11月18日 * */
public class MaxHeap extends Heap {
    public MaxHeap(int[] data) {
        super(data);
    }

    /** * {@inheritDoc} */
    public Heap buildHeap() {
        // 从最后一个节点的父节点开始构建堆
        int start = getParentIndex(length - 1);
        for (; start >= 0; start--) {
            adjustDownHeap(start);
        }
        return this;
    }

    /** * {@inheritDoc} */
    public Heap remove() {
        // 将最后一个节点与头结点交换
        swap(0, length - 1);
        // 重新复制一个数组
        int[] newData = new int[length - 1];
        System.arraycopy(data, 0, newData, 0, length - 1);
        this.data = newData;
        this.length = length - 1;
        // 从头开始调整堆
        adjustDownHeap(0);
        return this;
    }

    /** * {@inheritDoc} */
    public Heap insert(int value) {
        // 插入到数组最后
        int[] newData = new int[length + 1];
        System.arraycopy(data, 0, newData, 0, length);
        newData[length] = value;
        this.data = newData;
        this.length = length + 1;
        // 从最后一个节点开始自下而上调整堆
        adjustUpHeap(this.length - 1);
        return this;
    }

    /** * {@inheritDoc} */
    public void adjustDownHeap(int node) {
        int right = getRightChildIndex(node);// 右孩子
        int left = getLeftChildIndex(node);// 左孩子
        int max = node;// 三者最大的节点的索引
        if (right < length && data[right] > data[max]) {
            max = right;
        }
        if (left < length && data[left] > data[max]) {
            max = left;
        }
        if (max != node) {// 需要调整
            swap(node, max);
            adjustDownHeap(max);// 递归调整与根节点进行交换的节点,保证下层也是堆
        }
    }

    /** * {@inheritDoc} */
    public void adjustUpHeap(int node) {
        int parent = getParentIndex(node);// 父节点
        if (parent >= 0 && data[parent] < data[node]) {
            swap(node, parent);
            adjustUpHeap(parent);// 递归调整与根节点进行交换的节点,保证上层也是堆
        }
    }

    public static void main(String[] args) {
        int[] data = new int[10];
        for (int i = 0; i < data.length; i++) {
            data[i] = (int) (Math.random() * 100);
        }
        System.out.println(Arrays.toString(data));
        Heap heap = new MaxHeap(data);
        heap.buildHeap().print();
        heap.remove().print();
        heap.insert((int) (Math.random() * 100)).print();
    }
}

如上代码使用了继承,首先定义了一个堆的抽象类(Heap),封装了一些堆的公用方法,如:swap/getParentIndex/getRightChildIndex/getLeftChildIndex/print,其中具体的实现跟堆相关的方法通过抽象类由子类来实现,如:buildHeap/remove/insert/adjustHeap。大顶堆(MaxHeap)继承自Heap,实现具体的大顶堆的操作。小顶堆(MinHeap)的代码就不贴了,跟大顶堆相差不大,完整代码可见:GitHub。
下面结合上面的代码分别对堆的四个操作进行简单解释,都比较简单。

构建堆

首先构建堆,对于给定一个数组,我们需要将其构建成一个堆的结构,这个过程是一个递归的过程,相当于盖楼,先从最底层开始构建,并逐层往上构建,最终构建成一个完整的堆。
所以这里从最后一个节点的父节点开始构建堆,即构建最底层的堆,看图。

移除元素

从一个堆的结构中移除元素,一般都是移除堆顶元素,即最大值或者最小值,当我们移除堆顶元素之后,需要用堆的最后一个元素来填补到堆顶元素,然后这个堆的结构已经被破坏,我们需要调整这个堆,使其满足堆的结构。调整堆将在下面说。

插入元素

堆中插入元素一般是将其插入到堆的最后一个元素,然后自下而上递归调整堆。

调整堆

调整堆的方式有两种:自上而下和自下而上,分别对应移除堆和插入元素。但是两种方式的逻辑是一致的,即递归调整堆,如自上而下则寻找其两个子节点,将三者最大的放到根节点(三者小堆的根,不是整个大堆),然后递归调整移动了的节点(如右节点最大,则将右节点与根节点发生交换,递归调整右节点),最终到达底层,或者中途未发生交换则结束。自下而上原理类似,只是将一个节点与其父节点进行比较。

以上是我个人对堆的了解,如有错误请不宁赐教~

如果想了解更多关于堆的使用案例,可参考:

  • 堆排序可具体参考排序算法系列——堆排序
  • 优先队列可参考JDK并发工具类源码学习系列——PriorityBlockingQueue
参考文章

堆 (数据结构)
数据结构之堆

欢迎访问我的个人博客,寻找更多乐趣~

你可能感兴趣的:(数据结构,算法,堆,优先队列,堆排序)