排序算法之堆排序

1.堆

         堆是具有以下性质的完全二叉树:每个节点都大于或等于其左右孩子节点的值,称之为大顶堆;反之每个节点都小于或等于其左右孩子的值,称之为小顶堆。

          对于堆的实现,我们可以用数组来表示元素的位置关系,如果一个具有12个节点的堆(小顶堆)如下图所示:

                   

       那我们使用数组arr[0..11]的形式表示如下:

                        

       数中常见的函数形式如下所示:

              root = 0

              leftChild(i) = 2*i + 1

              rightChild(i) = 2*i +2

              parent(i) = (i-1)/2

             (如何证明不在本文做过多的描述,有兴趣的可以自己推算。提示:某节点的root层 到 该节点parent 层 的元素个数是等比数列)

 

2. 两个关键函数

        在讲解 堆排序前,先看看两个关键的函数。堆排序实现的原理,就是这两个函数实现的原理,我们先将该两个函数名定义为siftup 和 siftdown

 

siftup

        当x[0...n-1] 是堆时即heap(0,n-1),在x[n]位置放置一个元素,可能不满足x[0...n]具有堆的性质,即heap(0,n)不成立。那么siftup的作用就是通过调整使heap(0,n)成立。 该函数名就表明了实现的策列:将添加的新元素尽可能地向上筛选,向上筛选是通过和父节点交换位置来实现。 下图(从左到右)演示了添加 元素13 在堆中 向上交换的过程(在添加新元素13之前,该结构满足堆的性质)

        

         注意点:在上图中,除了带圈节点(新增节点或者新增节点被交换后的位置节点,后面用节点i表示) 和其父节点 可能不满足堆性质外,但其他地方都满足 堆的性质。那添加一个新节点siftup调整堆的伪代码就是 : 先for循环{

    判断节点i是否是root节点,如果是,则所有节点满足堆性质,退出循环;

           如果不是root 节点,找到节点i的父节点p,比较x[i]和x[p],如果x[i] >= x[p],则说明满足了堆的性质,退出循环;

                   如果x[i] < x[p],则交换x[i] 和x[p],交换后,i的位置索引 就是 p对应的位置索引,继续下一次for判断 

}

      (更详细的推导过程,包括为什么 仅仅 i节点和其父节点可能不满足堆的性质,可以见后面的参考文献)

 

siftdown

       如果x[1...n]满足堆的性质即heap(1,n),然后放一个新元素到x[0]中,这种情况就可以通过siftdown来调整,使其满足heap(0,n).该函数的实现策列:通过向下调整,直到它没有子节点或者小于等于子节点。下图演示了 节点18 添加到x[0]后 通过siftdown调整使之满足heap(0,n)

      

       注意点: 除了带圈 节点(新添加的节点或者新增节点被交换后的位置节点) 和其子节点不满足heap的性质外,其他的节点都是符合heap的性质的。那新增一个元素到根节点通过siftdown 调整堆的伪代码为:

       for(int i = 0;i<arr.length;i++){

         int c = 2*i+1 ;//左子节点

         if(c >= arr.length)  //说明没有子节点

             break;

         if(c+1 < arr.length){  //如果右子节点也存在

           if(arr[c+1] < arr[c]{ //如果右子节点比左子节点小,则说明左右子节点中较小的节点是右节点(后面称较小节点为S节点  )

                   c++; 

              }

         }

       if(arr[i] > arr[c]){    //节点 比 S节点 还要小,则交换 节点 和 S节点

          int temp= arr[c]

          arr[c] = arr[i]

         arr[i] = temp

         i  = c;  //i节点为 被交换后的位置节点

       }else{

          break;  

      }

  }

 

3. 堆排序实现

      理解了上面的两个重要函数后,再看看堆排序(用数组来表示堆,降序排序)的实现过程:

  1.  将数组第一个元素可以看成是一个排好序的堆,从第二个元素开始可以认为是向尾部添加新元素,使用siftup方法调整,使之heap(0,1)成立;以此类推直到heap(0,n-1)成立,这样小堆顶就建好了。
  2. heap(0,n-1) 建立好后,元素并不是完全排序的,但根据小顶堆的性质 我们知道根节点(0 节点)是最小的,通过交换0节点 和 n-1 节点,这样最小元素就放到了n-1的索引位置上,同时heap(1,n-2)满足堆,在根节点替被替换新元素后,通过siftdown,使heap(0,n-2)成立;然后再交换0节点和n-2节点... 循环该过程,最后仅仅剩下2个元素(根据堆的性质,第二个元素和第一个元素是排序好的,不用再交换位置,siftdown了)

     java代码参考:

package arithmetic;

/**
 * Created by ldxPC on 2018/10/23.
 */
public class HeapSort {

    private int getLeftChildIndex(int p){
         return 2*p+1;
    }
    private int getRightChildIndex(int p){
        return 2*p+2;
    }

    private int getParentIndex(int c){
        return (c-1)/2;
    }
    public void siftUp(int[] date){
          for(int i = 1 ;i<date.length;i++){
               int siftIndex = i;
               while(siftIndex>0 && date[getParentIndex(siftIndex)] > date[siftIndex]){
                    //swap
                    int tempValue = date[siftIndex];
                    date[siftIndex] = date[getParentIndex(siftIndex)];
                    date[getParentIndex(siftIndex)]= tempValue;
                   siftIndex = getParentIndex(siftIndex);
               }
          }
    }

    /**
     *
     * @param date
     * @param length 表示date的前多少个元素需要进行heap调整
     */
    public void siftDown(int[] date,int length){
         for(int i = 0;i<length;i++){
               int siftIndex = i;
              while(getLeftChildIndex(siftIndex) < length){
                    int lessChildIndex = getLeftChildIndex(siftIndex);
                    if(getRightChildIndex(siftIndex) < length){
                          if(date[getRightChildIndex(siftIndex)] < date[getLeftChildIndex(siftIndex)]){  //更小的child 是右节点,将lessChild+1
                               lessChildIndex++ ;
                          }
                    }

                    if(date[lessChildIndex] < date[siftIndex]){  //child节点比changeIndex节点小
                         int tempValue = date[siftIndex];
                         date[siftIndex] = date[lessChildIndex];
                         date[lessChildIndex] = tempValue;
                        siftIndex = lessChildIndex;
                    }else{
                        break;
                    }
              }
         }
    }

    public static void main(String[] args){

         int[] date = {4,3,6,1,79,4,7,32,1,33,14,64,68,35};

         System.out.println("数组调整前:"+printDate(date));

          HeapSort heapSort = new HeapSort();
          heapSort.siftUp(date); //构建堆

          for(int n = date.length-1;n>1;n--){
                 int lessValue = date[0];
                 date[0] = date[n];
                 date[n] = lessValue;

                 heapSort.siftDown(date,n-1); //对前n-1个数进行调整

          }

        System.out.println("数组调整后:"+printDate(date));

    }

    public static String printDate(int[] date){
        StringBuilder builder = new StringBuilder();
        for(int i = 0;i<date.length;i++){
             if(i >0){
                 builder.append(",").append(date[i]);
             }else{
                 builder.append(date[i]);
             }
        }

        return builder.toString();
    }
}

   打印结果:

    

4.总结

        之前学习堆排序的过程中,在网上看了一些blog,不过 一上来直接讲解了算法的流程(没有讲解本文中的两个重要的函数),看的是一知半解。 后来看到编程珠玑 第二版 第11章  对 堆排序的表述后,恍然大悟。  所以在我们学习算法的时候,有的时候需要对算法的本质或者说对算法的过程抽象 需要理解。  在理解本质后的基础上再学习该算法包括对算法的运用,就会事半功倍! 如果需要了解更详细的算法思路,可以自行参考 编程珠玑 第二版 第11章 

 

5.参考文献

    编程珠玑 第二版 第11章节

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值