排序算法——堆排序

 

1、堆排序思想

    1. 建立二叉树模型,依据数组下标获取节点关系:对于一个数组,我们可以将其看做是一个近似于完全二叉树的的广度遍历结果,该二叉树的结构特点是除了最底层元素,该树是完全充满的,而且是从左到右进行填充,数组[5,    2,6,    0,3,9,1,    7,4,8]对应的二叉树示例图如下

1309518-20180821163941926-1809327112.png

这样就可以保证数组中的每一个元素都对应二叉树的一个节点,而且依据数组下标就可以获得二叉树节点关系,关系获取公式为

输入当前节点下标i
    //获取左子节点下标
    public static int getLeft(int i){
        return 2 * i + 1;
    }
    //获取右子节点下标
    public static int getRight(int i){
        return 2 * i + 2;
    }

同样的,我们还发现一个规律,在这样的二叉树模型中每个非叶子节点数量为数组长度的一半,也就是下标为0到length/2-1的全都是非叶子节点。这一步很重要,必须要建立起数组下标与二叉树节点的对应关系。

    2. 建立最大堆(核心思想):所谓最大堆,也就是将树中的最大值上浮到根节点,也就是将数组中的最大值放在下标0处。

先来看看如果是一个只有2层的完全二叉树,那么很简单,就是将根节点与左右子节点进行比较,将比较后的结果放在根节点处即可。伪代码如下

输入根节点在数组的坐标index,以及数组的长度length
            int left = getLeft(index);//获取左子节点对应数组下标
            int right = getRight(index);//获取右子节点对应数组下标
            int max = index;//记录当前所在的下标
            
//进行比较并交换值
            if (left<length && arr[max] < arr[left]) {
                max = left;
            }
            if (right<length && arr[max] < arr[right]) {
                max = right;
            }
            if (max != index) {
                int value = arr[index];
                arr[index] = arr[max];
                arr[max] = value;
            }

如果是一个具有多层的完全二叉树结构,就可以将整个树结构两层一分,分为多个两层二叉树进行分析,从底层开始获取每两层中的最大值进行上浮,如下图所示,每个红圈都可以表示分解得到的子二叉树,只有两层,然后利用上面的那个算法将最大值放在子二叉树的根节点中,依照红圈上标记的数字次序(先从右到左,再从下到上,在数组中的表现形式就是遍历从下标 length/2-1遍历到下标0)遍历非叶子节点就可以实现将整个二叉树中的最大值上浮到根节点,也就是数组下标为0处

7ed6221e38ac024351666e1eaa046f91b93.jpg

上面说过,非叶子节点在数组中对应的下标范围为 0到length/2-1,那么对于一个数组,建立其最大堆的伪代码为

//建立最大堆,输入数组arr

        int length = arr.length;//建堆前获取要被建堆的元素数量
        int start =length/2 - 1;//获取第一个非叶子元素下标
        //从数组中下标最后一个非叶子节点往下标0遍历
        for (int index = start; index >= 0; index--) {
            //将每个非叶子节点及其左右子元素中的值进行比较,将最大值放在当前非叶子节点
            int left = getLeft(index);
            int right = getRight(index);
            int max = index;
            if (left<length && arr[max] < arr[left]) {
                max = left;
            }
            if (right<length && arr[max] < arr[right]) {
                max = right;
            }
            if (max != index) {
                int value = arr[index];
                arr[index] = arr[max];
                arr[max] = value;
            }
            
        }
        
    

    3. 在建立完毕最大堆后,就能保证数组中的最大值一定在下标0处,也就是二叉树的根节点处。此时我们将该值与数组末尾的值进行调换(arr[0]与arr[arr.length-1]),表示将该值或者说节点剔除,此时我们则可以重新对下标0到arr.length-2的值进行一次建堆,则可以在剩下的元素汇总重新找到一个最大值,在将此最大值放在数组的下标为arr.length-2处,以此类推,重复执行建堆以及剔除最大值的步骤就可以将整个数组进行排序。算法实现代码如下

/**
 * 
 * @Description:堆排序法,以建立最大堆为例
 * 首先要将未排序的数组元素看做是一个对二叉树结构上所有元素的的广度遍历结果,
 * 而且该二叉树为完全二叉树
 * 那么依据二叉树性质可以得到(数组长度即二叉树节点数为length,数组下标从1开始)
 * 每个非叶子节点(自身下标为i)的左子节点在数组中的下标为i*2,右子节点的下标为i*2+1
 * 每个节点的父节点下标为i/2
 * 每个叶子节点在数组中的下标一定是length/2之后,也就是length/2+1到length下标的都是叶子节点
 * 而每个非叶子节点在数组中的下标一定是1到length/2
 * 数组向二叉树转化完成
 * 
 * 再看看如何建立最大堆
 * 如果是对于一个只有两层高度的二叉树,那么对其建立最大堆的算法的过程应该是
 * (1)首先将根节点与左子节点比较,如果左子节点大于根节点,那么就将两个节点的值互换(也就是将数组两个下标的值调换)
 * (2)如果有右子节点,就再将根节点与右子节点比较,如果右子节点大于根节点,那么就将两个节点的值互换
 * (3)建立最大堆(也就是将最大值浮动到根节点)
 * 那么同理,对于多层高度的二叉树,应该从每一个叶子节点的父节点开始,作为一个子二叉树,执行建堆算法,
 * 建立完成后,再向上一层节点扩散,直至最后要保证整个二叉树的根节点是数组中的最大值,也就是数组中的第一个元素是最大值
 * 
 * 由建堆算法可知,对数组进行第一次建堆后数组下标1一定是最大元素,
 * 那么,如果从数组中第二个元素到最后一个元素再次执行一次建堆算法,
 * 就可以将第二个最大元素放在下标2的位置上,
 * 所以,从数组中下标1开始,一直到下标length-1进行遍历,每遍历到一个下标,就对该下标之后的
 * 元素进行一次建堆算法使得当前下标的元素值为最大值,这样最后就可以得到一个有序数组
 * 
 * 因为所有的操作都是在原数组进行,所以该算法空间复杂度可以说为O(n),而时间复杂度最坏情况为O(nlogn)
 */
public class Sort {
    //获取左子节点下标
    public static int getLeft(int i){
        return 2 * i + 1;
    }
    //获取右子节点下标
    public static int getRight(int i){
        return 2 * i + 2;
    }
    
    //建堆方法1,该方法主要是依次将建堆找到的最大值放在未剔除元素的末尾,表示该元素被剔除,
    //用i统计被剔除的元素数量,最后得到的是一个从小到大排序的数组
    public static int[] buildHeap1(int[] arr,int i){
        int length = arr.length - i;//建堆前获取剩下的要被建堆的元素数量,length也表示未剔除元素在数组中的最后一个下标
        int start =length/2 - 1;//获取第一个非叶子元素下标
        //开始进行建堆
        for (int index = start; index >= 0; index--) {
            int left = getLeft(index);
            int right = getRight(index);
            int max = index;
            if (left<length && arr[max] < arr[left]) {
                max = left;
            }
            if (right<length && arr[max] < arr[right]) {
                max = right;
            }
            if (max != index) {
                int value = arr[index];
                arr[index] = arr[max];
                arr[max] = value;
            }
            
        }
        //与未剔除元素在数组中的最后一个下标交互位置,表示剔除元素
        int swap = arr[0];
        arr[0] = arr[length-1];
        arr[length-1] = swap;
        
        //输出测试
        for(int value:arr){
            System.out.print(value+",");
        }
        System.out.println();
        return arr;
    }
    
    //建堆方法2,该方法与上述方法的区别是,建堆找到的最大值不会移动,就让其保持在根节点处
    //但是根节点在数组中的下标会向后移动,用i表示根节点偏移量就可以到最终得到一个从大到小排序的数组
    public static int[] buildHeap2(int[] arr,int i){
        int length = arr.length - i;//建堆前获取要被建堆的元素数量
        int start =i + length/2 - 1;//获取第一个非叶子元素下标
        
        //建堆
        for (int index = start; index >= i; index--) {
            int left = getLeft(index)-i;
            int right = getRight(index)-i;
            int max = index;
            if (left<arr.length && arr[max] < arr[left]) {
                max = left;
            }
            if (right<arr.length && arr[max] < arr[right]) {
                max = right;
            }
            if (max != index) {
                int value = arr[index];
                arr[index] = arr[max];
                arr[max] = value;
            }
            
        }
        //输出测试
        for(int value:arr){
            System.out.print(value+",");
        }
        System.out.println();
        return arr;
    }
    
    //堆排序
    public static int[] sortHeap(int[] arr){
        for (int i = 0;i<arr.length-1;i++){
            buildHeap1(arr, i);
        }
        return arr;
    }
    
    public static void main(String[] args) {
        int[] arr = {3,23,4,13,4,5,63,6,2};
        for(int i:sortHeap(arr)) {
            System.out.print(i+",");
        }
    }
}

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值