本文已参与「新人创作礼」活动,一起开启掘金创作之路。
堆排序原理
将数组结构在逻辑上当成完全二叉堆来看;然后将数组元素组成的无序二叉堆,构建成大顶堆或者小顶堆;利用 大顶堆/小顶堆 的性质 实现数组的排序。
这里有2个概念:
- 完全二叉堆:就是完全二叉树,叫法不同而已。 完全二叉树的官方定义为:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。
- 大顶堆 :堆顶值比左右子节点值大;小顶堆:堆顶值比左右子节点值小;
完全二叉树的官方定义中涉及到满二叉树的概念。什么是满二叉树呢?每一层的节点树达到最大值的二叉树是满二叉树。下面通过几幅图,来介绍完全二叉树和满二叉树;
排序的过程拆解(以大顶堆为例)
数组结构和堆结构的对应视图,如下
是将数组的结构看成一个个二叉堆的结构(实际的数据结构仍然为数组),这个时候的堆是不符合大顶堆的要求;大顶堆要求每一个堆的堆顶值最大;要每一个堆都要符合这个条件;
上面的堆,显然只有一个堆符合要求,其余的均不是大顶堆:
初始化堆
进行堆排序的第一步,就是要将这种无序的堆,初始化成大顶堆;
当完成堆的初始化之后,可以观察到,每一个堆的堆顶值都比其左右子节点的值大;最上面的堆的堆顶值是整个数组的最大值;
利用大顶堆排序
- 将堆顶的值与数组末尾的值进行交换并且交换之后,原堆顶的值不参与后续的堆调整;
- 剩下的(n-1)个数字,重新调整堆;
- 重复前2个步骤,样每一次都可以得到数组剩余的最大值;而每一次堆顶的值都依次放在数组的最后面,这样就可以对数组排序了;
代码部分
对任意堆的调整
我们上面讲到要将无序的堆调整成大顶堆,这就涉及到该数组的每一个堆结构;这样有一个问题,这毕竟是数组不是堆的结构;不能直观知道堆顶的2个左右字节点的值;
问题一:如何通过堆顶找到该堆的左右子节点?
我们通过上面的图,可以很直观的看到:堆顶的 下标和左右子节点的下标的关系:
-
堆顶下标:parent;
-
左节点下标:left;
-
右节点: left + 1;
-
left = parent * 2 +1 ;
通过下标关系我们就很容易通过堆顶找到左右子节点;
问题二:如何找到数组的最后一个堆的坐标?
我们通过上图可以看到一个数组有很多的堆结构,我们需要对每一个堆结构进行调整;我们选择首先从最后一个堆开始调整:
堆的堆顶坐标与数组长度的关系也很简单:maxIndex = length/2 -1 ; 楚上面的2点关系之后可以开始写代码了.
调整堆节点比较麻烦的点在于:当一个堆的子节点本身也是另一个堆的堆顶;如果调整了当前堆顶与子节点的值,会影响到该子节点作为堆顶的堆结构;这个时候就需要继续调整子节点作为堆顶所在的堆结构。
- 调节任意堆
/**调整任意堆:大顶堆*/
public void initFunction(int[]a,int parent,int length) {
int left = parent*2+1;
int x = a[parent];
int max = length/2 -1;
while(parent <= max) {
if(left+1 < length && a[left] < a[left+1])
left = left +1;
if(x>=a[left])break;
a[parent] = a[left];
parent = left;
left = 2*parent+1;
}
a[parent] = x;
}
- 初始化堆
/**初始化堆*/
public void initHeap(int[]a) {
int parent = a.length/2 -1;
while(parent >=0) {
initFunction(a,parent,a.length);
parent--;
}
}
- 堆排序
/**堆排序*/
public void heapSort(int[]a) {
initHeap(a);
int length = a.length;
while(length >0) {
int x = a[0];
a[0]=a[length-1];
a[length-1]=x;
length--;
initFunction(a,0,length);
}
}