数据结构(11)堆(优先队列)的原理与代码实现

堆(优先队列)

堆的介绍

在说到堆之前一定要先说一下优先队列,我们都知道队列是一种特殊的线性表,它允许在一端插入在另一端删除(先进先出),而优先队列是带有优先级的队列,优先队列把进入队列中的元素分优先级,出队列时首先选择优先级最高的元素出队列,对于优先级相同的元素则按照先进先出的原则出队列。优先队列在生活中有许多的应用。

实现优先队列,可以用链表、二叉查找树、二叉堆,其中二叉堆对于优先队列的实现是相当普遍的,所以一般我们也把(二叉)堆称为优先队列。

堆本质上是一颗完全二叉树,对于完全二叉树,我们是可以将它转化为数组存储的(如下图),所以堆也可以用数组来存储。

在这里插入图片描述

堆的分类

堆又可以细分为最小堆最大堆。对于任意一颗子树,它的父节点小于任意一个子节点(对于左节点于右节点的大小没有规定),则称这个堆为最小堆;相反,如果它的父节点大于任意一个子节点,则称这个堆为最大堆;这个特点也称为堆序属性。对于最小堆,由于最小节点在根节点上,利用这个特性我们可以快速找到min结点;对于最大堆,由于最大节点在根节点上,利用这个特性我们可以快速找到max结点,正因为这个属性,所以堆才成为实现优先队列的最普遍选择。

代码实现

为了避免冗余,接下来的代码只讲最小堆,最小堆至少需要下列两种操作:

  1. insert 插入
  2. deleteMin 删除优先级最小者

先说最小堆的插入操作

如下图所示,我们要在一个已知的最小堆 [13,21,16,19,31,19,68,65,26,32] 中插入元素X=14,我们先在下一个可用位置创建一个空穴并将14放入,如果 14 放入目前的位置上并不破坏堆的序,那么插入完成;但是由于 14 < 31,破坏了最小堆的序,所以我们将空穴的父节点中的元素与空穴进行调换,这样空穴就朝着根的方向进了一步。重复该过程直到满足堆的序。
在这里插入图片描述
代码:

/*
    插入元素操作
        1. 将元素插入堆中,要保持堆的堆序性质
        2. 允许重复的元素
            如何保持堆的堆序性质?
            步骤:我们将新插入的元素放入下一个可用位置的空穴,如果放入后满足最小堆,则结束;
            否则将空穴的父节点元素移入该空穴中,空穴则朝着根的方向移动一步,这样反复,直到满足最小根
     */
    public void insert(int x){
        //插入前得先判断数组的情况,如果数组现在是满的,则需要扩容
        if(currentSize == array.length){
           // enlargeArray(array.length +1);
            array = Arrays.copyOf(array, array.length+1);
        }

        //将x放入到数组的空穴中
        array[currentSize] = x;
        //将x放入正确的位置上
        siftUp(currentSize);
        //有效个数++
        currentSize++;

    }

    //由下向上调整堆的结构,专业点称为上滤(percolate up)
    private void siftUp(int x) {
        if(x <= 0) return;

        //如果目标节点小于父节点,则将父节点与目标节点对换
        while(array[x] < array[getParent(x)]){
            int temp = array[x];
            array[x] = array[getParent(x)];
            array[getParent(x)] = temp;

            //每次交换完后改变控制变量x的坐标
            x = getParent(x);//将父节点的左边赋值给x

            //如果最后到了堆的根节点,则跳出循环
            if(x <= 0)
                break;

        }
    }

最小堆的deleteMin()

如图,我们现有一个最小堆 [13, 14, 16, 19, 21, 19, 68, 65, 26, 32, 31] ,我们打算删除这个堆中的最小者,即位于根位置的节点,但是删除根节点后为了维持最小堆的序,我们换要进行一系列操作。第一步:删除根结点(最小值);第二步:我们将堆中的最后一个元素 31 移动到空闲的根节点处,但是因为 31 大于他的两个子节点,不符合最小堆了;所以第三步:我们将 31 与它的子节点中最小值 14 进行交换,这样31就向下移动了一层;当 31 移动到新的位置后,由于此时31还是大于它的子节点,所以我们重复第三步直到放入正确的位置
在这里插入图片描述
代码:

//找到最小的,根结点
    public int findMin(){
        //判断数组是否为空
        if(isEmpty()){
            throw new ArrayIndexOutOfBoundsException("数组为空,没有最小值");
        }
        return array[0];

    }

    /*
    删除最小的
    思路:当删除掉最小元素后,根结点处就成了空穴,为了维持最小堆,我们得进行一系列操作
    首先因为删除了一个元素,为了维持最小堆,所以得移动数组的最后一个元素
    最简单的做法是将最后一个元素放入空穴中,但这样显然不符合最小堆,所以我们的做法是将空穴的两个儿子中最小者放入空穴中
    但这样一来,就又形成了一个空穴,我们按照上面的步骤,再将这个空穴的子节点中最小的放进来..直到最后删除成功
     */
    public int deleteMin(){
        int i = findMin();//找到最小值

        //删除元素前得判断,是否数组就只有一个元素
        if(currentSize == 1){
            array = new int[DEFAULT_CAPACITY - 1];
            return i;
        }

        //将堆中的最后一个元素放入最小值的位置(根)
        array[0] = array[currentSize-1];
        //有效元素--
        currentSize--;
        //对数组进行处理,即删除最小值后,数组长度会发生变化的
        array = Arrays.copyOf(array, array.length-1);
        //从上到下调整堆结构
        siftDown(0);

        //将最小值返回
        return i;
    }



    //由上向下调整堆结构;专业点称为下滤(percolate down)
    private void siftDown(int x) {
        int j = -1;//用于记录和x交换位置的坐标
        int num = array[x];//num 用来记录x坐标的数字

        //当x坐标有左子节点时进入
        for(; getleft(x) < currentSize; x = j){
            j = getleft(x); //默认j为左子节点的坐标

            //如果右子节点元素<左子节点元素,就将j变为右子节点的坐标
            if(array[getright(x)] < array[getleft(x)]){
               // array[x] = array[getright(x)];
                j = getright(x);
            }

            //如果子节点元素 < 目前的父节点元素,则将子节点赋值给父节点
            if(array[j] < array[x]){
                array[x] = array[j];
                array[j] = num;
            }

            else
                break;

        }
        array[x] =num;//x已经发生了变换
    }

有必要提一句:堆的每个操作的时间复杂度都为O(logN)

完整代码

package Structures;

import java.util.Arrays;

/**
 * @author Emma
 * @create 2020 - 03 - 21 - 23:51
 * 最小堆结构
 */
public class MinHeap {
    //堆结构,由一个数组和一个代表当前堆的大小的数字组成
    private static final int DEFAULT_CAPACITY = 1; //默认容量
    private int[] array; //堆数组
    private int currentSize; //堆中的有效元素数

    /*
    有两种构造方法:
        1.是给定堆的容量后,再将元素一个一个insert进来
        2.直接将一个数组赋值到堆里
     */

    //构造函数
    public MinHeap() {
        this(DEFAULT_CAPACITY);
    }

    //指定堆的容量,默认是10
    public MinHeap(int capacity) {
        //有效元素为0
        currentSize = 0;
        //创建数组
        array = new int[capacity];
    }

    //构造函数,直接将一个数组赋值到堆里
    public MinHeap(int[] data) {
        //有效元素为data中的所有元素
        currentSize = data.length;

        //创建数组
        array = new int[data.length];

        //赋值
        int x = 0;
        for(int i : data){
            array[x++] = i;
        }

    }

    /*
    插入元素操作
        1. 将元素插入堆中,要保持堆的堆序性质
        2. 允许重复的元素
            如何保持堆的堆序性质?
            步骤:我们将新插入的元素放入下一个可用位置的空穴,如果放入后满足最小堆,则结束;
            否则将空穴的父节点元素移入该空穴中,空穴则朝着根的方向移动一步,这样反复,直到满足最小根
     */
    public void insert(int x){
        //插入前得先判断数组的情况,如果数组现在是满的,则需要扩容
        if(currentSize == array.length){
           // enlargeArray(array.length +1);
            array = Arrays.copyOf(array, array.length+1);
        }

        //将x放入到数组的空穴中
        array[currentSize] = x;
        //将x放入正确的位置上
        siftUp(currentSize);
        //有效个数++
        currentSize++;

    }

    //由下向上调整堆的结构
    private void siftUp(int x) {
        if(x <= 0) return;

        //如果目标节点小于父节点,则将父节点与目标节点对换
        while(array[x] < array[getParent(x)]){
            int temp = array[x];
            array[x] = array[getParent(x)];
            array[getParent(x)] = temp;

            //每次交换完后改变控制变量x的坐标
            x = getParent(x);//将父节点的左边赋值给x

            //如果最后到了堆的根节点,则跳出循环
            if(x <= 0)
                break;

        }
    }



    //找到最小的,根结点
    public int findMin(){
        //判断数组是否为空
        if(isEmpty()){
            throw new ArrayIndexOutOfBoundsException("数组为空,没有最小值");
        }
        return array[0];

    }

    /*
    删除最小的
    思路:当删除掉最小元素后,根结点处就成了空穴,为了维持最小堆,我们得进行一系列操作
    首先因为删除了一个元素,为了维持最小堆,所以得移动数组的最后一个元素
    最简单的做法是将最后一个元素放入空穴中,但这样显然不符合最小堆,所以我们的做法是将空穴的两个儿子中最小者放入空穴中
    但这样一来,就又形成了一个空穴,我们按照上面的步骤,再将这个空穴的子节点中最小的放进来..直到最后删除成功
     */
    public int deleteMin(){
        int i = findMin();//找到最小值

        //删除元素前得判断,是否数组就只有一个元素
        if(currentSize == 1){
            array = new int[DEFAULT_CAPACITY - 1];
            return i;
        }

        //将堆中的最后一个元素放入最小值的位置(根)
        array[0] = array[currentSize-1];
        //有效元素--
        currentSize--;
        //对数组进行处理,即删除最小值后,数组长度会发生变化的
        array = Arrays.copyOf(array, array.length-1);
        //从上到下调整堆结构
        siftDown(0);

        //将最小值返回
        return i;
    }



    //将x坐标,由上向下调整堆结构
    private void siftDown(int x) {
        int j = -1;//用于记录和x交换位置的坐标
        int num = array[x];//num 用来记录x坐标的数字

        //当x坐标有左子节点时进入
        for(; getleft(x) < currentSize; x = j){
            j = getleft(x); //默认j为左子节点的坐标

            //如果右子节点元素<左子节点元素,就将j变为右子节点的坐标
            if(array[getright(x)] < array[getleft(x)]){
               // array[x] = array[getright(x)];
                j = getright(x);
            }

            //如果子节点元素 < 目前的父节点元素,则将子节点赋值给父节点
            if(array[j] < array[x]){
                array[x] = array[j];
                array[j] = num;
            }

            else
                break;

        }
        array[x] =num;//x已经发生了变换
    }

    //展示
    public void show(){
        for(int i : array){
            System.out.print(i+"  ");
        }
        System.out.println();
    }

    //获取到输入结点的父节点的坐标
    private int getParent(int i){
        if(i <= 0)  return -1;
        return (i-1)>>1;

    }

    //获取到输入结点的左节点的坐标
    private int getleft(int i){
        return (i*2)+1;

    }

    //获取到输入结点的y右节点的坐标
    private int getright(int i){
        return (i*2)+2;
    }

    //判断数组是否为空
    private boolean isEmpty() {
        return currentSize == 0 || array.length == 0;
    }

    //增加数组的容量
   /* private void enlargeArray(int newSize){
        int[] old = array;
        array = new int[newSize];
        //复制
        int i = 0;
        for(int x : old){
            array[i++] = x;
        }
    }*/

    //减少数组的容量
    /*private void enSmallArray(int newSize){
        int[] old = array;
        array = new int[newSize];
        //复制
        for(int i = 0; i <newSize; i++){
            array[i] = old[i];
        }
    }*/

    public static void main(String[] args) {
        //测试方法

        //定义一个空堆,并删除其中的最小值
        MinHeap heap1 = new MinHeap();
        heap1.show();
        heap1.insert(1);
      //  heap1.insert(2);
        heap1.show();
        heap1.deleteMin();
     //   heap1.deleteMin();    //会报错,没有问题
        heap1.show();
      //  System.out.println(heap1.deleteMin());  //会报错,没有问题

       //定义一个堆,并将一个满足最小堆的数组赋值进去,这个数组的长度比默认容量大,可以自动扩容
        int[] arr = {13,21,16,19,31,19,68,65,26,32};
        MinHeap heap2 = new MinHeap(arr);
        heap2.show();
        heap2.insert(14);
        heap2.show();//插入14后show

        int[] arr2 = new int[]{13, 14, 16, 19, 21, 19, 68, 65, 26, 32, 31};
        MinHeap heap3 = new MinHeap(arr2);
        heap3.deleteMin();
        heap3.show();

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值