@(Study)[计算机, 算法]

理论简介

  1. 建立初始堆
  2. 首末元素互换, 即得到最大元素放入数组最末尾.
  3. 调整堆. 第二步的操作明显会将堆破坏, 所以需要调整堆.
  4. 跳回第二步.

建立初始堆

在建堆之前需要将数组转成二叉树图, 方便理解:

完全二叉树形式

如果将父>左子|右子当做树的最小单元组, 称为父子单元, 那么只需要保证每个父子单元满足最大堆规则, 那么整体树就满足了最大堆.
==>定义一个方法(unitAdjust())用来调整父子单元, 将单元中最大的值推到该单元的根部, 成为, 原来的降到最大值之前的位置, 作为. 但是这样有可能使得以这个新子为父结点的下一层的父子单元不满足最大堆规则. ==>设置规则, 如果发生了交换, 则递归调用方法自身, 调整新子为父结点的父子单元. 如此反复.

上述过程可以说是自上而下的下钻过程.

但是如果使得整个树成为最大堆, 那么就是需要针对树中的每个父子单元进行调整. 只需要对树中的每个非叶子节点进行遍历, 使用上述方法unitaAdjust()调整即可. 进一分析, 可以发现这里有个问题: 遍历的方向是自上而下还是自下而上呢?

如果自上而下会出现什么情况呢?
如下图, 第一趟循环中, 操作的对象是以根结点34为父结点的父子单元. 循环体内部将调用unitAdjust(), 34和50将互换, 由于发生了交换, unitAdust()方法将发生递归调用, 去操作以新子34为父结点的父子单元, 经过比较, 未发生交换情况, 第一趟循环结束. 图中发生了比较或者互换的位置以绿色标出, 这时聪明的你不难发现, 当程序向下遍历走到第三个节点时, 便发生了重复比较, 当然你或许能够想到规避重复比较的方法, 但是我是没有想出, 或者也可以认为重复比较无所谓.

clipboard.png

其实, 换一种思路, 进行自下而上遍历情况就完全不一样了.

  1. 从最后一个非叶子节点67开始, 6790
  2. 50节点不动
  3. 2390, 触发下钻机制.

    1. 2367
  4. 3490, 触发下钻机制

    1. 3467, 再次下钻

      1. 未动

最终得到初始堆如下图所示, 其中蓝色表示交换过位置.

clipboard.png

可见, 整体是自下而上遍历, 具体单元操作时采取自上而下, 总之, 自上而下自下而上的相结合.

建立堆的过程其实有点冒泡的味道, 数值大逐渐的冒到上面, 好像密度大的即使开始在水底部, 但随着遍历, 密度大的终将上浮到上部.

堆排序

初始堆建立好了后, 堆顶的元素自然就是最大值, 于是将第一个元素和最后一个元素互换, 即完成将最大值放到最末尾, 完了第一次排序.

由于交换使得堆被破坏, 所以需要对的n-1个元素进行调整, 使其成为堆. 具体做法就是调用unitAdjust()以根节点为父结点的父子单元进行调整, 刚换来的最末位元素自然会被移到子节点位置, 这样递归调用机制被触发, 于是继续保证所有下层调整为堆.

接着将倒数第二个元素和堆顶元素互换, ......如此反复.

代码实现

思路:

//建立初始堆//
for (i=n/2;i>=1;i--) {
    unitAdjust()
}
//堆排序//
for (i=n; i>=1; i--) {
    首末交换
    unitAdjust()调整
}

Java代码:

测试结果:

  • 10亿长度, 内存爆掉;
  • 1亿长度, 35693ms.
package LearningLog;

/*
 * - 10亿长度, 内存爆掉
 * - 1亿长度, 35693ms.
 */

public class demo25 {
    public static void main(String[] args) {
        int[] arr=MyJava.randomArr(100000000, 10000,123);
//        System.out.println(Arrays.toString(arr));
//        initHeap(arr);
        long t0 = System.currentTimeMillis();
        heapSort(arr);
        long t1 = System.currentTimeMillis();
        System.out.println((t1-t0)+"ms");
//        System.out.println(Arrays.toString(arr));
    }
    /* ====unitAdjust()===========================================
     * 父子单元调整
     * I: 父结点的索引, 数组长度: 非常非常非常关键, 因为后续调用的时候, 需要卡住最大的遍历位置, 否则将会将尾部已经排好序的较大数值顶到上面去了, 那就糟糕呕吐了
     */
    public static void unitAdjust(int array[],int fatherNode, int sub_size) {
        int lchild = 2*fatherNode+1;
        int rchild = lchild+1;

        int max = fatherNode; // 默认最大值索引就是父结点的索引
        int tmp = 0;
        if (fatherNode<sub_size/2) {  //只对数组的非叶子节点操作
            if (lchild<sub_size && array[fatherNode]<array[lchild]) {
                max = lchild;
            }
            if (rchild<sub_size && array[max]<array[rchild]) {  //rchild<N 首先的保证有 右子
                max = rchild;
            }
            if (max != fatherNode) { //最大值不再是父结点时, 需要交换
                tmp = array[fatherNode];
                array[fatherNode] = array[max];
                array[max] = tmp;
                
                unitAdjust(array, max, sub_size); //如果发生交换, 则触发递归调用, 保证下层的满足堆
            }
        }
    }
    
    /*
     * ====initHeap()============================================
     * 建立初始堆
     */
    public static void initHeap(int[] array) {
        // TODO Auto-generated method stub
        for (int i=array.length/2-1; i>=0; i--) {
            unitAdjust(array, i, array.length);   //大的值不断上浮
        }

    }
    
    /*
     * ====heapSort()=================================================
     * 堆排序
     */
    public static void heapSort(int[] array) {
        int tmp=0;
        //建立堆//
        initHeap(array);
        for(int i=array.length-1; i>=0; i--) {
            //首尾互换/
            tmp = array[i];
            array[i] = array[0];
            array[0] = tmp;
            
            //剩下的i-1个元素需要调整//
            unitAdjust(array, 0, i); //始终只用调整第一个父子单元即可
        }
    }
    
    
    
    
    
}






Nisus
200 声望6 粉丝

如果生命只有一次, 我愿意尝试各种可能!