堆排序与其在topN案例的应用

一、堆排序概述

作为选择排序的改进版,堆排序可以把每一趟元素的比较结果保存下来,以便我们在选择最小/大元素时对已经比较过的元素做出相应的调整。
堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点,当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。
大顶堆与小顶堆
算法思想(以大顶堆为例):

  1. 将长度为n的待排序的数组进行堆有序化构造成一个大顶堆
  2. 将根节点与尾节点交换并输出此时的尾节点
  3. 将剩余的n -1个节点重新进行堆有序化
  4. 重复步骤2,步骤3直至构造成一个有序序列

二、构造堆

假设待排序数组为[20,50,10,30,70,20,80]
在构造有序堆时,我们开始只需要扫描一半的元素(n/2-1 ~ 0)即可,为什么?
因为(n/2-1)~0的节点才有子节点,如下图,n=8,(n/2-1) = 3 即3 2 1 0这个四个节点才有子节点
初始状态
第一步从3开始,比较3与其子节点32+1和32=2,把最大的和3位置上的数字互相交换;
然后从下到上,从右到左,依次调整所有的非叶子结点;
在这里插入图片描述
这里注意图四,80和20交换后,由于交换前80有子节点,交换可能打破平衡,所以对于有子节点的节点替换后应该再次调整它使堆保持平衡。
至此有序堆已经构造好了!如上图四。

三、调整堆

(1)堆顶元素80和尾40交换后–>调整堆
在这里插入图片描述
(2)堆顶元素70和尾30交换后–>调整堆
在这里插入图片描述
(3)堆顶元素60尾元素20交换后–>调整堆
在这里插入图片描述
(4)其他依次类推,最终已排好序的元素如下:
在这里插入图片描述

四、代码实现

import java.util.Arrays;

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[]{20, 50, 21, 40, 70, 10, 80, 30, 60};

        //最后一位下标
        int lastIndex = arr.length - 1;

        //arr.length / 2 - 1;堆特性只有这个数量的节点有子节点
        for (int i = lastIndex / 2 - 1; i >= 0; i--) {
            constructHeap(arr, i, lastIndex);
        }
        System.out.println(Arrays.toString(arr));
        //[80, 70, 21, 60, 50, 10, 20, 30, 40]

        while (lastIndex > 0) {
            //把堆顶的元素换到末尾
            swap(arr, 0, lastIndex);
            //末尾不参与置换
            lastIndex--;
            //对堆结构进行调整,最后一位不参与调整
            constructHeap(arr, 0, lastIndex);
        }

        System.out.println(Arrays.toString(arr));
        //[10, 20, 21, 30, 40, 50, 60, 70, 80]
    }

    /**
     * 对数组的某个父节点进行操作,比较其与子节点的大小,将最大的置换到父节点位置
     * @param arr       给定数组
     * @param i         处理哪一个父节点
     * @param lastIndex 参与交换的数组尾标。可以指定小于数组长度,代表后面的几个数不参与交换
     */
    private static void constructHeap(int[] arr, int i, int lastIndex) {
        if (lastIndex <= 0) {
            return;
        }
        //堆特性,左节点右节点index
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        if (right > lastIndex) {
            //只比较left和父节点哪个大放到父节点
            if (arr[left] > arr[i]) {
                swap(arr, left, i);
                //与父节点交换的节点是否有子节点,如果有的话需要递归
                if (left <= lastIndex / 2 - 1) {
                    constructHeap(arr, left, lastIndex);
                }
            }
        } else {
            //比较三个最大的和父节点交换
            int sonMaxIndex = arr[left] >= arr[right] ? left : right;
            if (arr[sonMaxIndex] > arr[i]) {
                swap(arr, sonMaxIndex, i);
                //与父节点交换的节点是否有子节点,如果有的话需要递归
                if (sonMaxIndex <= lastIndex / 2 - 1) {
                    constructHeap(arr, sonMaxIndex, lastIndex);
                }
            }
        }
    }

    private static void swap(int[] arr, int son, int father) {
        int temp = arr[father];
        arr[father] = arr[son];
        arr[son] = temp;
    }
}

五、topN实践

大多数的选择排序算法在topn应用中性能都不错,因为topn只需要取出最大或最小的n个数就可以停止了。
这里举一个曾优化过的案例分享给大家:

  • 需求:某电商app需要实时统计每天的topn热销商品,要求就是每天、实时。老的项目组使用Flink WindowAll所有数据放到一个集合然后使用jdk自带的排序实现topn计算,初期还好,后来订单量激增后,产生背压,mq积压等问题。
  • 原因:之后分析其使用的Trigger为每条数据都会触发计算,jdk的sort方法性能一般,在数据量并发量大时执行缓慢
  • 优化:trigger指定n条数据触发一次窗口计算,排序算法优化为堆排序。优化后性能提高一个数量级以上。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值