数据结构系列——堆

来源:互联网 发布:rf流体软件 编辑:程序博客网 时间:2024/05/22 01:33

堆(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 vicky01200059@163.com * @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 vicky01200059@163.com * @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
参考文章

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


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

0 0
原创粉丝点击