• 堆的原理和实现


    一、前言

      本文将详细为大家讲解关于堆这种数据结构。学了本章以后我们会发现,呃呵,原来...名字听起来高大上的数据结构也就那么回事。

      后面会持续更新数据结构相关的博文。

      数据结构专栏:https://www.cnblogs.com/hello-shf/category/1519192.html

      git传送门:https://github.com/hello-shf/data-structure.git

    二、堆

      堆这种数据结构,有很多的实现,比如:最大堆,最小堆,斐波那锲堆,左派堆,斜堆等。从孩子节点的个数上还可以分为二叉堆,N叉堆等。本文我们从最大二叉堆堆入手看看堆究竟是什么高大上的东东。

      2.1、什么是堆

      我们先看看它的定义

    1 堆是一种完全二叉树(不是平衡二叉树,也不是二分搜索树哦)
    2 堆要求孩子节点要小于等于父亲节点(如果是最小堆则大于等于其父亲节点)

      满足以上两点性质即可成为一棵合格的堆数据结构。我们解读一下上面的两点性质

      1,堆是一种完全二叉树,关于完全二叉树,在我另一篇博客《二分搜索树》中有详细的介绍,要注意堆是一种建立在二叉树上的数据结构,不同于AVL或者红黑树是建立在二分搜索树上的数据结构。

      2,堆要求孩子节点要大于等于父亲节点,该定义是针对的最大堆。对于最小堆,孩子节点小于或者等于其父亲节点。

      如上所示,只有图1是合格的最大堆,图2不满足父节点大于或者等于孩子节点的性质。图3不满足完全二叉树的性质。

       2.2、堆的存储结构

      前面我们说堆是一个完全二叉树,其中一种在合适不过的存储方式就是数组。首先从下图看一下用数组表示堆的可行性。

      看了上图,说明数组确实是可以表示一个二叉堆的。使用数组来存储堆的节点信息,有一种天然的优势那就是节省内存空间。因为数组占用的是连续的内存空间,相对来说对于散列存储的结构来说,数组可以节省连续的内存空间,不会将内存打乱。

      接下来看看数组到二叉堆的下标表示。将数组的索引设为 i。则:

      左孩子找父节点:parent(i)= (i - 1)/2。比如2元素的索引为5,其父亲节点4的下标parent(2)= (5 - 1)/2 = 2;

      右孩子找父节点:parent(i)= (i-2)/ 2。比如0元素找父节点 (6-2)/2= 2;

      其实可以将上面的两种方法合并成一个,即parent(i)= (i - 1)/2;从java语法出发大家可以发现,整数相除得到的就是省略了小数位的。所以。。。你懂得。

      同理

      父节点找左孩子:leftChild(i)= parent(i)* 2 + 1。

      父节点找右孩子:rightChild(i) = parent(i)*2 + 2。

     三、最大二叉堆的实现

      3.1、构建基础代码

      上面分析了数组作为堆存储结构的可行性分析。接下来我们通过数组构建一下堆的基础结构

     1 /**
     2  * 描述:最大堆
     3  *
     4  * @Author shf
     5  * @Date 2019/7/29 10:13
     6  * @Version V1.0
     7  **/
     8 public class MaxHeap<E extends Comparable<E>> {
     9     //使用数组存储
    10     private Array<E> data;
    11     public MaxHeap(){
    12         data = new Array<>();
    13     }
    14     public MaxHeap(int capacity){
    15         data = new Array<>(capacity);
    16     }
    17     public int size(){
    18         return this.data.getSize();
    19     }
    20     public boolean isEmpty(){
    21         return this.data.isEmpty();
    22     }
    23 
    24     /**
    25      * 根据当前节点索引 index 计算其父节点的 索引
    26      * @param index
    27      * @return
    28      */
    29     private int parent(int index) {
    30         if(index ==0){
    31             throw new IllegalArgumentException("该节点为根节点");
    32         }
    33         return (index - 1) / 2;//这里为什么不分左右?因为java中 / 运算符只保留整数位。
    34     }
    35 
    36     /**
    37      * 返回索引为 index 节点的左孩子节点的索引
    38      * @param index
    39      * @return
    40      */
    41     private int leftChild(int index){
    42         return index*2 + 1;
    43     }
    44 
    45     /**
    46      * 返回索引为 index 节点的右孩子节点的索引
    47      * @param index
    48      * @return
    49      */
    50     private int rightChild(int index){
    51         return index*2 + 2;
    52     }
    53 }

      3.2、插入和上浮 sift up

      向堆中插入元素意味着该堆的性质可能遭到破坏,所以这是如同向AVL中插入元素后需要再平衡是一个道理,需要调整堆中元素的位置,使之重新满足堆的性质。在最大二叉堆中,要堆化一个元素,需要向上查找,找到它的父节点,大于父节点则交换两个元素,重复该过程直到每个节点都满足堆的性质为止。这个过程我们称之为上浮操作。下面我们用图例描述一下这个过程:

      如上图5所示,我们向该堆中插入一个元素15。在数组中位于数组尾部。

      如图6所示,向上查找,发现15大于它的父节点,所以进行交换。

      如图7所示,继续向上查找,发现仍大于其父节点14。继续交换。

      然后还会继续向上查找,发现小于其父节点19,停止上浮操作。整个二叉堆通过上浮操作维持了其性质。上浮操作的时间复杂度为O(logn)

      插入和上浮操作的代码实现很简单,如下所示。

     1     /**
     2      * 向堆中添加元素
     3      * @param e
     4      */
     5     public void add(E e){
     6         // 向数组尾部添加元素
     7         this.data.addLast(e);
     8         siftUp(data.getSize() - 1);
     9     }
    10 
    11     /**
    12      * 上浮操作
    13      * @param k
    14      */
    15     private void siftUp(int k) {
    16         // 上浮,如果大于父节点,进行交换
    17         while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
    18             data.swap(k, parent(k));
    19             k = parent(k);
    20         }
    21     }

       

       3.3、取出堆顶元素和下沉 sift down

      上面我们介绍了插入和上浮操作,那删除和下沉操作将不再是什么难题。一般的如果我们取出堆顶元素,我们选择将该数组中的最后一个元素替换堆顶元素,返回堆顶元素,删除最后一个元素。然后再对该元素做下沉操作 sift down。接下来我们通过图示看看一下过程。

      如上图8所示,将堆顶元素取出,然后让最后一个元素移动到堆顶位置。删除最后一个元素,这时得到图9的结果。

     

      如图10,堆顶的9元素会分别和其左右孩子节点进行比较,选出较大的孩子节点和其进行交换。很明显右孩子17大于左孩子15。即和右孩子进行交换。

      如图11,9节点继续下沉最终和其左孩子12交换后,再没有孩子节点。此次过程的下沉操作完成。下沉操作的时间复杂度为O(logn)

      代码实现仍然是非常简单

     1     /**
     2      * 取出堆中最大元素
     3      * 时间复杂度 O(logn)
     4      * @return
     5      */
     6     public E extractMax(){
     7         E ret = findMax();
     8         this.data.swap(0, (data.getSize() - 1));
     9         data.removeLast();
    10         siftDown(0);
    11         return ret;
    12     }
    13 
    14     /**
    15      * 下沉操作
    16      * 时间复杂度 O(logn)
    17      * @param k
    18      */
    19     public void siftDown(int k){
    20         while(leftChild(k) < data.getSize()){// 从左节点开始,如果左节点小于数组长度,就没有右节点了
    21             int j = leftChild(k);
    22             if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){// 选举出左右节点最大的那个
    23                 j ++;
    24             }
    25             if(get(k).compareTo(get(j)) >= 0){// 如果当前节点大于左右子节点,循环结束
    26                 break;
    27             }
    28             data.swap(k, j);
    29             k = j;
    30         }
    31     }

      3.4、Replace和Heapify

      Replace操作呢其实就是取出堆顶元素然后新插入一个元素。根据我们上面的总结,大家很容易想到。返回堆顶元素后,直接将该元素置于堆顶,然后再进行下沉操作即可。

     1     /**
     2      * 取出最大的元素,并替换成元素 e
     3      * 时间复杂度 O(logn)
     4      * @param e
     5      * @return
     6      */
     7     public E replace(E e){
     8         E ret = findMax();
     9         data.set(0, e);
    10         siftDown(0);
    11         return ret;
    12     }

       Heapify操作就比较有意思了。Heapify本身的意思为“堆化”,那我们将什么进行堆化呢?根据其存储结构,我们可以将任意一个数组进行堆化。将一个数组堆化?what?一个个向最大二叉堆中插入不就行了?呃,如果这样的话,需要对每一个元素进行一次上浮时间复杂度为O(nlogn)。显然这样做的话,时间复杂度控制的不够理想。有没有更好的方法呢。既然这样说了,肯定是有的。思路就是将一个数组当成一个完全二叉树,然后从最后一个非叶子节点开始逐个对飞叶子节点进行下沉操作。如何找到最后一个非叶子节点呢?这也是二叉堆常问的一个问题。相信大家还记得前面我们说过parent(i) = (child(i)-1)/2。这个公式是不分左右节点的哦,自己可以用代码验证一下,在前面的parent()方法中也有注释解释了。那么最后一个非叶子节点其实就是 ((arr.size())/2 - 1)即可。

      接下来我们通过图示描述一下这个过程,假如我们将如下数组进行堆化

      第一步:转化为一棵完全二叉树,如图12所示。

      

      第二步:找到最后一个非叶子节点,如图13所示。这里我们将还未调整的非叶子节点设为黄色,将即将要调整的置为绿色。调整完成的置为绿边圆。

      

      第三步:下沉,非叶子节点和左右孩子进行比较,选出最大的孩子节点进行交换。交换结果如图14所示

      第四步:找到下一个非叶子节点。

     

      第五步:下沉。

      

      第六步:找到下一个非叶子节点。

     

      第七步:下沉。

     

      第八步:找到下一个非叶子节点。

      第九步:下沉。30节点下沉到56元素的位置,然后继续下沉,但是发现大于23,下沉结束。

      第十步:找到下一个非叶子节点。

      第十一步:下沉。对17节点进行下沉操作,直到其直到适合自己的位置。

      Heapify的整个过程就完成了。时间复杂度控制在了O(n)。

       代码实现非常的简单。

     1     /**
     2      * Heapify
     3      * @param arr
     4      */
     5     public MaxHeap(E[] arr){
     6         data = new Array<>(arr);
     7         for(int i = parent(arr.length - 1); i >= 0; i --){
     8             siftDown(i);
     9         }
    10     }

     四、完整代码

      

      1 /**
      2  * 描述:最大堆
      3  *
      4  * @Author shf
      5  * @Date 2019/7/29 10:13
      6  * @Version V1.0
      7  **/
      8 public class MaxHeap<E extends Comparable<E>> {
      9     //使用数组存储
     10     private Array<E> data;
     11     public MaxHeap(){
     12         data = new Array<>();
     13     }
     14     public MaxHeap(int capacity){
     15         data = new Array<>(capacity);
     16     }
     17 
     18     /**
     19      * Heapify
     20      * @param arr
     21      */
     22     public MaxHeap(E[] arr){
     23         data = new Array<>(arr);
     24         for(int i = parent(arr.length - 1); i >= 0; i --){
     25             siftDown(i);
     26         }
     27     }
     28     public int size(){
     29         return this.data.getSize();
     30     }
     31     public boolean isEmpty(){
     32         return this.data.isEmpty();
     33     }
     34 
     35     /**
     36      * 根据当前节点索引 index 计算其父节点的 索引
     37      * @param index
     38      * @return
     39      */
     40     private int parent(int index) {
     41         if(index ==0){
     42             throw new IllegalArgumentException("该节点为根节点");
     43         }
     44         return (index - 1) / 2;//这里为什么不分左右?因为java中 / 运算符只保留整数位。
     45     }
     46 
     47     /**
     48      * 返回索引为 index 节点的左孩子节点的索引
     49      * @param index
     50      * @return
     51      */
     52     private int leftChild(int index){
     53         return index*2 + 1;
     54     }
     55 
     56     /**
     57      * 返回索引为 index 节点的右孩子节点的索引
     58      * @param index
     59      * @return
     60      */
     61     private int rightChild(int index){
     62         return index*2 + 2;
     63     }
     64 
     65     /**
     66      * 向堆中添加元素
     67      * 时间复杂度 O(logn)
     68      * @param e
     69      */
     70     public void add(E e){
     71         // 向数组尾部添加元素
     72         this.data.addLast(e);
     73         siftUp(data.getSize() - 1);
     74     }
     75 
     76     /**
     77      * 上浮操作
     78      * 时间复杂度 O(logn)
     79      * @param k
     80      */
     81     private void siftUp(int k) {
     82         // 上浮,如果大于父节点,进行交换
     83         while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
     84             data.swap(k, parent(k));
     85             k = parent(k);
     86         }
     87     }
     88 
     89     /**
     90      * 获取 index 索引位置的元素
     91      * 时间复杂度 O(1)
     92      * @param index
     93      * @return
     94      */
     95     private E get(int index){
     96         return this.data.get(index);
     97     }
     98 
     99     /**
    100      * 查找堆中的最大元素
    101      * 时间复杂度 O(1)
    102      * @return
    103      */
    104     public E findMax(){
    105         if(this.data.getSize() == 0){
    106             throw new IllegalArgumentException("当前heap为空");
    107         }
    108         return this.data.get(0);
    109     }
    110 
    111     /**
    112      * 取出堆中最大元素
    113      * 时间复杂度 O(logn)
    114      * @return
    115      */
    116     public E extractMax(){
    117         E ret = findMax();
    118         this.data.swap(0, (data.getSize() - 1));
    119         data.removeLast();
    120         siftDown(0);
    121         return ret;
    122     }
    123 
    124     /**
    125      * 下沉操作
    126      * 时间复杂度 O(logn)
    127      * @param k
    128      */
    129     public void siftDown(int k){
    130         while(leftChild(k) < data.getSize()){// 从左节点开始,如果左节点小于数组长度,就没有右节点了
    131             int j = leftChild(k);
    132             if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){// 选举出左右节点最大的那个
    133                 j ++;
    134             }
    135             if(get(k).compareTo(get(j)) >= 0){// 如果当前节点大于左右子节点,循环结束
    136                 break;
    137             }
    138             data.swap(k, j);
    139             k = j;
    140         }
    141     }
    142 
    143     /**
    144      * 取出最大的元素,并替换成元素 e
    145      * 时间复杂度 O(logn)
    146      * @param e
    147      * @return
    148      */
    149     public E replace(E e){
    150         E ret = findMax();
    151         data.set(0, e);
    152         siftDown(0);
    153         return ret;
    154     }
    155 }

      

      如有错误的地方还请留言指正。

      原创不易,转载请注明原文地址:https://www.cnblogs.com/hello-shf/p/11393655.html 

  • 相关阅读:
    socket.io+angular.js+express.js做个聊天应用(二)
    [原创]Python通过Thrift连接HBase
    [原创]安装Sqoop并验证
    使用PostgreSQL、Hibernate 构建 NoSQL
    [原创]HBase客户端开发举例(第三部分)
    [原创]HBase客户端开发举例(第二部…
    [原创]全分布模式下Hadoop安装
    Samba的基本配置
    常见设计模式举例 转载有改动
    【转载】hibernate中使用ehcache
  • 原文地址:https://www.cnblogs.com/hello-shf/p/11393655.html
Copyright © 2020-2023  润新知