Heap Sort 堆排序与 Top K 问题(Java集合实现)

  现代基础性计算环境中,输入量的元素规模N会非常大,但有时候会只要求从中找出K个最大(或最小)的元素,即Top K问题。如果使用之前介绍的传统排序算法,先对N个元素进行全排序然后再取前K个元素,计算代价会变的非常高昂。因为我们实际上只需要Top K元素的排序,而剩余元素的详细排序结果我们其实并不care。而本文介绍的Heap Sort堆排序不仅是一种高效的排序算法,还可以很好地解决Top K问题

二叉堆

定义

 堆 Heap, 一种特殊的树状数据结构。在堆中,对于任意节点都有其值恒大于等于或小于等于其子节点的值,前者称之为max heap最大堆,后者则称之为min heap最小堆。常见的堆有:二叉堆、多叉堆、斐波那契堆等。这里我们所使用的即是二叉堆,其在形式上是一个完全二叉树

  一般地,我们通过数组来实现二叉堆,将堆中元素按从上到下、从左到右的层级顺序依次存储到数组中即可(一般地为简便起见,通常从数组的第二个位置开始存储元素,不使用数组索引为0的位置。故大小为N的堆,所需数组的空间大小为N+1)。下图即为一个最大堆的存储示意图(为简便起见,下文中的"堆"均特指"二叉堆")

 在一个堆中,对于位置为k节点,根据上图可知,其父节点在数组中的位置索引为 floor(k/2),其两个子节点在数组中的位置索引分别为2k、2k+1

堆有序化

堆有序:当一颗二叉树的任意节点恒大于等于或小于等于其两个子节点时,即被称为堆有序

 本文我们以最大堆为例进行介绍,最小堆与其大同小异,此处将不再赘述。具体地,当一个堆的根节点元素被较小的元素替换时,我们就需要自上而下地恢复堆的有序化;当向堆底插入一个新节点时,我们则需要自下而上地恢复堆的有序化

** 1. 自上而下的堆有序化(下沉) **
  当一个最大堆的有序状态因为某个节点比其(两个或其中一个)子节点更小而被打破时,那么可以通过将该节点与其两个子节点中的较大者交换来恢复堆,该过程形象化地被称为"下沉"。交换后,可能会导致其在子节点处依然会继续打破堆的有序化,故我们需要不断地使用相同的方式将其进行修复,将节点向下移动直到其子节点均比其更小或到达了堆的底部。下图即是一个值为3的节点通过"下沉"恢复堆的有序化的过程

** 2. 自下而上的堆有序化(上浮) **
  当一个最大堆的有序状态因为某个节点比其父节点更大而被打破时,那么可以通过将该节点与其父节点交换来恢复堆,该过程形象化地被称为"上浮"。交换后,可能会导致其在父节点处依然会继续打破堆的有序化,故我们需要不断地使用相同的方式将其进行修复,将节点向上移动直到其父节点节点比其更大或成为了堆的根节点。下图即是一个值为25的节点通过"上浮"恢复堆的有序化的过程

java实现

 Java的ArrayList是一个自动扩容的动态数组列表,故可用其实现最大堆。实现代码如下所示

public class HeapSort{
   //以ArrayList实现堆
    private static ArrayList<Integer> maxHeap;
    public static void main(String[] args) {
    Integer[] array= {3,7,10,1,4,9,8,5,2,6,1};
    maxHeap = new ArrayList<>();
    maxHeap.add(null);//零位置赋空
    maxHeap.addAll(Arrays.asList(array));
    int N = getSize();
    sort(N);
    System.out.println(maxHeap);
}
/**
     * 堆排序算法
     * @param N
     */
    public static void sort(int N){
        //1.首先构造大根堆,从最后一个父节点开始,其效率更高
        for(int i=N/2 ; i>0 ; i--){
            sink(i,N);
        }
        //2.堆排序完成,则最大值在第一位
        while(N>1){
            swap(1,N);//将最大值放在最后面
            N--;//更新堆大小
            sink(1,N);//3.继续使用上浮,对剩余堆继续进行有序化
        }
    }
    private  static int getSize(){
        return maxHeap.size()-1;
    }


    /**
     * 上浮
     */
    public  static void swim(int k) {
        //k表示list中的下标位置,如果k不是根节点,比较此节点和其父结点的值
        while( k>1 && compare(k/2,k)){
            //父节点的值<子节点的值
            swap(k/2,k);
            k = k/2;
        }
    }

   /*  下沉
     * k表示当前的结点下标
     * N表示堆的大小
     * @param k
     * @param N
     */
    public  static void sink(int k,int N){
        while(k*2 <= N){//说明有左结点
            int childNode = 2*k;
            if(childNode < N && compare(childNode,childNode+1)){//说明有右子结点,并且左子结点值小于右子结点
                childNode = childNode + 1;
            }
            if(!compare(k,childNode)){//说明父节点大于两个子节点的值
                break;
            }
            //不跳出则就要交换,说明父节点会比子节点小
            swap(k,childNode);
            //交换k和childNode
            k = childNode;
        }
    }

    /**
     * 比较大小
     * @param index1
     * @param index2
     * @return
     */
    public  static boolean compare(int index1,int index2){
        int number1 = maxHeap.get(index1);
        int number2 = maxHeap.get(index2);
        if(number1 < number2){
            return true;
        }else{
            return false;
        }
    }

    /**
     * 交换位置
     * @param index1
     * @param index2
     */
    public static void swap(int index1,int index2){
        int element1 = maxHeap.get(index1);
        int element2 = maxHeap.get(index2);
        maxHeap.set(index1,element2);
        maxHeap.set(index2,element1);
    }
}

Heap Sort 堆排序

算法思想

 通过上文,我们知道构造一个大小为N的最大堆,我们就可通过根节点获取N个元素中最大的元素。这里我们以对N个元素进行升序排列为例,讲解堆排序的算法思想:

  1. 将N个元素构造成一个最大堆,其中array[0]位置不使用
  2. 从一个大小为N的最大堆中将根节点(N个元素中最大的元素)array[1]从堆中移出,放入数组最后的位置中,则array[N]元素排序完成
  3. 对剩下的大小为N-1的最大堆,重新进行堆的有序化: 将堆底元素array[N-1]放入根节点的位置array[1],并通过"下沉"重现建立新堆的有序化
  4. 重复上述Step 2-3,直到最大堆中只剩一个元素时为止。此时N个元素即排序完成

 上面的代码的sort方法就是实现堆排序的方法,很多人可能不理解为什么要从下标为N/2开始构造呢,因为这种做法更加高效。因为当一个父节点的两个子堆都是有序时,此时在该父节点上调用"下沉"方法,即可将它们变成一个堆。前者需要N次"上浮"才能将元素一个一个地添加到堆中以此来完成堆的构建;而后者则只需floor(N/2)次"下沉"即可将堆构建完成,因为一个大小为N的完全二叉树,叶子节点的数量至少为N/2个
测试结果如下:

Top K问题

解决方案

 容易想到的是解决方案是,对N个元素进行排序,然后根据排序结果从中取出最大(或最小)的K个元素,但是当N的规模非常之大时,效率会非常低。而堆则可以很好解决地该问题。这里,我们以取出最小的K个元素为例,说明如何通过最大堆解决该问题(若是找出最大的K个元素,则应通过最小堆解决):

  1. 建立一个最大堆,遍历N个元素并重复下述 Step 2-3
  2. 若堆的大小未达到K时,直接将新元素放入堆底,然后通过"上浮"恢复堆的有序化
  3. 若堆的大小已经为K时,则比较根节点元素(即堆中最大元素)是否比新元素大。若是,则直接用新元素来替换该堆的根节点,并通过"下沉"恢复堆的有序化;若不是,则直接丢弃该新元素即可
  4. 当N个元素全部遍历完成后,此时堆中元素即为所要找出最小的K个元素

Java实现

  private static ArrayList<Integer> maxHeap;
    public static void main(String[] args) {
        int [] array= {3,7,10,1,4,9,8,5,2,6,1};
        maxHeap = new ArrayList<>();
        maxHeap.add(null);
        Top_k(array,3);
        System.out.println(maxHeap);
    }

    /**
     * top_k问题其实是构成了一个k个结点的堆
     * @param array
     * @param k
     */
    public static void  Top_k(int[] array,int k ){
        for(int i=0;i<array.length;i++){
            int size = maxHeap.size()-1;
            if(size < k){//堆未满
                maxHeap.add(array[i]);
             swim(size+1);
            }else{//堆满
                Integer maxElement = maxHeap.get(1);
                //数组元素小于最大值
                if(array[i] < maxElement){
                    maxHeap.set(1,array[i]);
                    //放入值后下沉
                    sink(1,size);
                }
            }
        }
    }

运行结果:

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值