@(Study)[计算机, 算法]
理论简介
- 建立初始堆
- 首末元素互换, 即得到最大元素放入数组最末尾.
- 调整堆. 第二步的操作明显会将堆破坏, 所以需要调整堆.
- 跳回第二步.
建立初始堆
在建堆之前需要将数组转成二叉树图, 方便理解:
如果将父>左子|右子
当做树的最小单元组, 称为父子
单元, 那么只需要保证每个父子
单元满足最大堆规则, 那么整体树就满足了最大堆.
==>定义一个方法(unitAdjust()
)用来调整父子
单元, 将单元中最大的值推到该单元的根部, 成为父, 原来的父降到最大值之前的位置, 作为子. 但是这样有可能使得以这个新子为父结点的下一层的父子
单元不满足最大堆规则. ==>设置规则, 如果发生了交换, 则递归调用方法自身, 调整新子
为父结点的父子
单元. 如此反复.
上述过程可以说是自上而下的下钻过程.
但是如果使得整个树成为最大堆, 那么就是需要针对树中的每个父子
单元进行调整. 只需要对树中的每个非叶子节点进行遍历, 使用上述方法unitaAdjust()
调整即可. 进一分析, 可以发现这里有个问题: 遍历的方向是自上而下还是自下而上呢?
如果自上而下会出现什么情况呢?
如下图, 第一趟循环中, 操作的对象是以根结点34
为父结点的父子单元
. 循环体内部将调用unitAdjust()
, 34和50将互换, 由于发生了交换, unitAdust()
方法将发生递归调用, 去操作以新子
34为父结点的父子单元
, 经过比较, 未发生交换情况, 第一趟循环结束. 图中发生了比较或者互换的位置以绿色标出, 这时聪明的你不难发现, 当程序向下遍历走到第三个节点时, 便发生了重复比较, 当然你或许能够想到规避重复比较的方法, 但是我是没有想出, 或者也可以认为重复比较无所谓.
其实, 换一种思路, 进行自下而上遍历情况就完全不一样了.
- 从最后一个非叶子节点67开始, 67↔90
- 50节点不动
-
23↔90, 触发下钻机制.
- 23↔67
-
34↔90, 触发下钻机制
-
34↔67, 再次下钻
- 未动
-
最终得到初始堆如下图所示, 其中蓝色表示交换过位置.
可见, 整体是自下而上遍历, 具体单元操作时采取自上而下, 总之, 自上而下和自下而上的相结合.
建立堆的过程其实有点冒泡
的味道, 数值大逐渐的冒到上面, 好像密度大的即使开始在水底部, 但随着遍历, 密度大的终将上浮
到上部.
堆排序
初始堆建立好了后, 堆顶的元素自然就是最大值, 于是将第一个元素和最后一个元素互换, 即完成将最大值放到最末尾, 完了第一次排序.
由于交换使得堆被破坏, 所以需要对的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); //始终只用调整第一个父子单元即可
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。