手撕 堆 的建立与算法实现

目录

1.什么是堆

1.1  堆的相关概念

1.2   完全二叉树

1.3   根据堆的性质可以得出如下结论

2. 最大堆的建立与算法实现

2.1 底层逻辑实现(新建堆)

2.2 常用工具方法实现

2.3 添加元素

2.4 移除优先级最高的元素

2.5  replace操作(取出最大元素,放入一个新元素)

2.6 时间复杂度分析

3. 关于堆的其他算法实现

3.1  heapify操作(将任意数组堆化)

3.2  堆排序

4 . 测试

4.1  测试添加和移除优先级最高的元素

4.2  测试heapify和堆排序

5.总结


1.什么是堆

1.1  堆的相关概念

     (最大)堆
       是一个可以被看成一棵树的数组对象,满足如下性质:
      
       a. 堆中的父亲结点总大于或等于其左右孩子结点的值
       b. 总是一棵完全二叉树


1.2   完全二叉树

      定义:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

       如上图,第h层所有节点连续集中在最左边

       我们这里来实现一个最大堆(值越大优先级越高),最小堆也可参考本文实现

       我们说堆是可以被看成一棵树的数组对象,也就是说,它的底层实现是数组,我们这里把元素依次排列成完全二叉树的形状,根节点是最大值,这就是一个最大堆,例 : 上图就是一个最大堆

1.3   根据堆的性质可以得出如下结论

     1> 根节点没有父亲结点
     2> 除根节点之外的任意结点(i)的父亲结点的索引为: parent = i/2
     3>任意结点的左孩子结点的索引为: leftIndex = 2 * i
     4>任意结点的右孩子结点的索引为: rightIndex = 2 * i +1
上面的结论是根结点存储在索引为1的位置,如果根结点存储在索引为0的位置时,会得到:
parent = (i- 1)/2;     leftIndex = 2 * i + 1;   rightIndex = 2 * I + 2;
有了以上依据,我们就可以来建立起一个最大堆

2. 最大堆的建立与算法实现

2.1 底层逻辑实现(新建堆)

//最大堆实现
public class MaxHeap<T extends Comparable<T>> {    //继承Comparable接口

    private T[] array;   //底层数组
    private int size;    //元素个数

    public MaxHeap() {       //无参构造
        this.size = 0;
        this.array = (T[]) new Comparable[20];
    }

    public MaxHeap(T[] array) {    //有参构造
        this.array = Arrays.copyOf(array, array.length);  //复制数组
        this.size = array.length;
    }
}

这里我们先给出基础的成员变量和构造方法

2.2 常用工具方法实现

    //判断是否为空
    public boolean isEmpty() {
        return this.size == 0;
    }

    //获取堆中有多少元素
    public int getSize() {
        return this.size;
    }

    //获取父节点索引
    private int getParentIndex(int index) {
        if (index < 0) {
            throw new IllegalArgumentException("索引值错误");
        } else if (index == 0) {
            return -1;
        } else {
            return (index - 1) / 2;
        }
    }

    //获取左孩子索引
    private int getLeftChildIndex(int index) {
        if (index < 0) {
            throw new IllegalArgumentException("索引值错误");
        } else {
            return 2 * index + 1;
        }
    }

    //交换
    private void swap(int curIndex, int changeIndex) {    //当前索引,变换的索引
        T temp = this.array[curIndex];
        this.array[curIndex] = this.array[changeIndex];
        this.array[changeIndex] = temp;
    }

    @Override
    public String toString() {     //重写toString()方法
        StringBuilder sb = new StringBuilder("[");
        int i = 0;
        while (i < size) {
            sb.append(this.array[i]);
            if (i != size - 1) {
                sb.append(",");
            }
            i++;
        }
        sb.append("]");
        return sb.toString();
    }

我们的底层数据结构是数组,所以都是对索引进行操作

右孩子索引为 : 左孩子索引 + 1    上面方法也就不去实现这一步了

这里也是给出了我们一会儿要用到的工具方法

2.3 添加元素

这里添加元素的时候,我们需要在添加完毕做一个浮动操作

       如上图,我们添加了一个 52 的结点, 是在数组的最末尾 , 但根据最大堆的性质 , 52 在上图中应该是比 41 大 ,比 62 小的 这样一个位置, 所以我们在添加完成后需要 为52 找到一个合适的位置 , 我们把这一操作 叫做上浮

过程 :  1添加元素52,添加到数组中索引为size的位置,然后更新size

            2、从最后一个结点开始与父亲结节进行(优先级)比较,如果父亲结点的优先级低于当前结点,则进行交换

            3、重复第二步操作,

            4直至根节点或父亲结点的优先级高于当前结点

直到满足最大堆的性质,操作结束 ,代码实现 : 

    //添加元素
    public void add(T ele) {
        this.array[size] = ele;  //添加元素
        this.size++;             //更新size
        floatUp(size - 1);       //调用浮动方法
    }

    //浮动操作
    private void floatUp(int i) {
        int curIndex = i;  //当前操作的节点
        int parentIndex = getParentIndex(curIndex);  //得到父节点
        //循环条件 : 当前节点大于0 并且 当前节点优先级大于它的父亲节点
        while (curIndex > 0 && this.array[curIndex].compareTo(this.array[parentIndex]) >0){
            swap(curIndex, parentIndex);   //交换值
            curIndex = parentIndex;  //让当前节点指向父节点
            parentIndex = getParentIndex(curIndex); //父节点指向当前节点的父节点
        }
    }

2.4 移除优先级最高的元素

   最大堆中优先级最高的元素是索引为 0 的元素  
    也就是说,我们要删除优先级最高的元素, 那么这里新问题也就来了 ,删除了之后,我们是需要重新找出一个优先级最高的元素 如下图 :

       我们可以看出, 如果删除62 ,我们肯定会用52 当作优先级最高的元素,但我们能去直接删除62吗,我们的底层是数组 , 要删除元素的话最好的选择是先保存然后覆盖这个值,所以我们这里可以用末尾的元素(图中16)去替换他 ,然后再把16去放到底下一个合适的位置,这个操作我们叫做下沉

过程 : 

1 、使用最后一个元素替换索引为 0 元素,更新 size

 2、从索引为0的位置开始进行下沉操作

     下沉操作:
     1> 找到当前结点左右孩子结点中优先级较高的结点
     2> 如果当前结点的优先级小于左右孩子结点中优先较高的结点,则进行交换

     3> 重复第 2 步操作
3 直至叶子结点或左右孩子结点中优先级较高结点小于当前结点的优先级
至此,操作结束

代码实现如下 : 

    //移除优先级最高的元素
    public T removePriorityFirst() {
        if (isEmpty()) {    //为空
            throw new IllegalArgumentException("堆为null");
        }
        T result = this.array[0];   //保存根元素
        this.array[0] = this.array[this.size - 1];  //让数组末尾元素覆盖数组索引为0的元素
        this.size--;         //更新size
        swim();  //根下沉
        return result;
    }

    //下沉操作
    private void swim() {
        if (isEmpty()) {
            return;
        }
        int curIndex = 0;     //当前节点
        int leftIndex = getLeftChildIndex(curIndex);  //获取左孩子结点索引
        int changeIndex = leftIndex;      //先默认左边优先级最大
        //循环的条件 : 有左右孩子
        while (leftIndex < this.size) {
            //如果右孩子结点优先级高,让changeIndex指向右孩子结点
            if (leftIndex + 1 < this.size && this.array[leftIndex].compareTo(this.array[leftIndex + 1]) < 0) {
                changeIndex = leftIndex + 1;
            }
            //找到了合适的位置,跳出循环
            if (this.array[curIndex].compareTo(this.array[changeIndex]) > 0) {
                break;
            }
            swap(curIndex, changeIndex);  //交换
            curIndex = changeIndex;   //当前节点指向优先级高的结点
            leftIndex = getLeftChildIndex(curIndex);  //获取左孩子结点索引
            changeIndex = leftIndex;    //默认左孩子优先级高
        }
    }

至此,操作结束

2.5  replace操作(取出最大元素,放入一个新元素)

直接看代码实现,这里我们只需要调我们的swim方法即可

    //替换操作:取出优先级最高的元素,将其替换为一个新元素
    public void replace(T ele) {
        this.array[0] = ele;
        swim();    //调我们的下沉方法即可
    }

2.6 时间复杂度分析

从上面的分析图中,可以得出:无论进行上浮还是下沉操作,最多交换的次数为整棵树的高度h
                                               O(h) = O(logn)

3. 关于堆的其他算法实现

3.1  heapify操作(将任意数组堆化)

        heapify: 将任意数组整理成堆的形状
        给你一个任意排列的数组,将数组整理成一个堆
        
        实现方式:从最后一个元素的父亲结点开始进行调整(sift down),直到根结点

      思路:  我们从最后一个节点的父亲节点往前依次调整,每次都同自己的孩子结点比较做一个下沉操作 ,直到调整到根节点结束,调整顺序如下

 根据索引顺序依次向前找,每次都同自己的孩子结点做swim操作

1 、找到最后一个元素的父亲结点。 (size-1-1)/2
2 、循环进行下沉操作,直至根结点
代码实现如下 :
    //堆化操作:给定一个数组,将其整理成堆
    public void heapify() {           //时间复杂度为o(n)
        if (this.array == null || this.array.length == 0) {
            return;
        }
        int lastParentIndex = (this.array.length - 1 - 1) / 2; //获取最后一个结点的父亲结点的索引
        //循环到根节点
        for (; lastParentIndex >= 0; lastParentIndex--) {
            heapifySwim(this.array, lastParentIndex, this.array.length);  //每次做一个swim操作
        }
    }

    //heapify中的swim操作
    private void heapifySwim(T[] array, int lastParentIndex, int length) {
        //array:原数组  lastParentIndex:最后一个结点的父亲结点的索引  length:原数组长度
        if (isEmpty()) {
            return;
        }
        int curIndex = lastParentIndex;
        int leftIndex = getLeftChildIndex(curIndex);
        int changeIndex = leftIndex;
        while (leftIndex < length) {
            if (leftIndex + 1 < length && array[leftIndex].compareTo(array[leftIndex + 1]) < 0) {
                changeIndex = leftIndex + 1;
            }
            if (array[curIndex].compareTo(array[changeIndex]) > 0) {
                break;
            }
            swap(curIndex, changeIndex);
            curIndex = changeIndex;
            leftIndex = getLeftChildIndex(curIndex);
            changeIndex = leftIndex;
        }
    }

至此操作结束

3.2  堆排序

   要求:先把数组进行heapify操作,整理成堆

排序过程:
     1> 先将第一个元素与最后一个元素交换,保证优先级最大的元素放到最后。然后对除最后一个结点之外的这棵树进行下沉操作
     2> 在将第一个元素与倒数第二个元素交换,然后对除最后 2 个结点之外的这棵树进行下沉操作
     3> 以此类推,直到根节点

代码实现 : 

这里我们直接调用我们写好的方法就行

    //堆排序
    public void sort(T[] array) {
        if (array == null || array.length == 0) {
            return;
        }
        //Arrays.stream(array).forEach(this::add);
        this.heapify();  //堆化
        int index = 0;
        while (!isEmpty()) {
            array[index++] = this.removePriorityFirst();  //排序
        }
    }

至此操作完成

4 . 测试

我们用main方法来测试刚才的算法

4.1  测试添加和移除优先级最高的元素

添加:

     Integer[] arr = new Integer[]{ 62, 30, 41, 28, 13, 22, 19, 15, 17, 16};
     MaxHeap<Integer> heap = new MaxHeap<>();   //传入arr数组
     for (Integer i : arr) {
        heap.add(i);  //添加
     }
     System.out.println(heap);

随便给一个数组,然后遍历依次添加元素,输出这个堆,得到结果如下 : 

我们来画图看他是不是一个最大堆

可见,测试通过 

移除优先级最高的元素 : 

    System.out.println(heap.removePriorityFirst());
    System.out.println(heap);

结果 : 

画图验证 :  

测试通过

4.2  测试heapify和堆排序

    Integer[] arr1 = new Integer[]{ 30, 62, 41, 28, 13, 22, 19, 15, 17, 16};
    MaxHeap<Integer> heap1 = new MaxHeap<>(arr1);
    heap1.heapify();   //堆化
    System.out.print("堆化 : ");
    System.out.println(heap1);
    heap1.sort(arr1);  //排序
    System.out.print("堆排序 : ");
    System.out.println(Arrays.toString(arr1));

结果 : 

 测试通过

5.总结

     堆是一种很重要的数据结构,这里介绍了最大堆的一些常见的算法实现,另外后续会在介绍优先队列里使用堆作为我们的底层数据结构,后面再来介绍,感谢阅读

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值