堆排序
堆排序是利用最大堆或者最小堆这种数据结构设计的排序算法。本文只介绍最大堆,最小堆的原理是相同的。
最大堆
堆排序算法中一般使用二叉堆,最大堆中的子节点值小于父节点的值,因此最大堆的根节点值最大。通过不断的将根节点值移到堆的末尾,并且调整堆使其满足最大堆的特性,达到排序的目的。
数组可以用来表示一个完全二叉树,对于数组索引i的节点,其两个孩子节点(若有)的索引分别为2xi+1、2xi+2。因此通过数组可以非常方便地维护一个最大堆。
将输入数组构建成最大堆
举例说明如何将输入的待排序数组构建成最大堆,假如我们有一个输入数组arr:
{4,8,5,3,13,11,9,2,7,1}
这个数组的完全二叉树表示如图1:
图1 输入数组的完全二叉树表示
从堆中最后一个非叶子节点开始调整堆,这里可以看出调整堆是自底向顶调整的,当调整到某个节点A的时候,该节点的孩子节点对应的树都已经调整成最大堆了。
对于图1来说,首先调整的是节点13。因为我们需要调整成最大堆,对于根节点13的子树来说,13的节点值大于它的子树节点值,已经符合最大堆的条件,因此该子树不需要调整。
接下来调整节点3,它有两个孩子,左孩子的值为2,右孩子的值为7,将最大的值调整到该子树的根节点,调整后的根节点为7,左孩子为2,右孩子为3,参考图2所示。
图2
接着调整节点5,它有两个孩子,左孩子的值为11,右孩子的值为9,该子树调整后的根节点为11,左孩子为5,右孩子为9,参考图3所示。
图3
接着调整节点8,它的左孩子为7,右孩子为13,这三个节点最大值为13,调整后的根节点为13,左孩子为7,右孩子为8。注意,这次调整后的左孩子没有变化,仍然是7,但是右孩子有变化,变为8,且以右孩子节点为根节点的子树还有子树,这次调整可能会破坏节点8为根节点的子树最大堆特性,因此需要重新调整(递归调整)。本次调整后的最大堆参考图4。
图4
接着调整节点4,它的左孩子为13,右孩子为11,最大值为13,调整后的根节点为13,左孩子为4,右孩子为11。注意,这样调整后右孩子没有变化,但是左孩子变为4,这会破坏左子树的最大堆特性,需要重新调整左子树使其满足最大堆特性。本次调整后的最大堆参考图5。
图5
到此为止,原输入数组已经构建成了最大堆,最大堆的根节点为13。
排序
怎么通过最大堆来将输入数组排序呢?最大堆构建完成后我们将堆顶元素和堆的最后一个节点元素交换,这样堆的最后一个节点就是堆的最大值,同时将堆的大小减1,并继续调整二叉树使其满足最大堆的特性。重复此步骤,最后的输入数组就是排序的了。
将图5中的根节点和堆的末尾节点交换,参考图6。
图6
节点13处已经是有序的了,不需要再调整,根节点为1的树此时显然不符合最大堆的特性,需要重新调整(不包括节点13)。调整后的最大堆参考图7。
图7
继续将最大堆的根节点11和堆的末尾(节点3)交换,调整后的堆参考图8。
图8
继续将最大堆的根节点9和堆的末尾节点2交换,调整后的堆参考图9。
图9
继续将最大堆根节点8和末尾节点1交换,调整后的堆参考图10。
图10
重复此步骤,直到堆调整完为止,最终的二叉树参考图11。
图11
到此为止,将原来输入数组通过最大堆完成排序了,图11从上往下,从左往右,遍历结果为
1, 2, 3, 4, 5, 7, 8, 9, 11, 13
已经是排序的。
代码实现
通过上面的说明,已经对堆排序的原来有了详细理解,接下来看下代码实现:
/**
* <p>文件描述: 堆排序</p>
*
* @Author luanmousheng
* @Date 17/8/1 下午3:46
*/
public class HeapSort {
/**
* 调整数组arr位置k处的堆,将位置k和子节点进行比较,将较大的值放在位置k处,重新调整子树为最大堆
* @param arr 需要调整的数组
* @param k 需要调整的节点索引
* @param size 堆的大小
*/
private static void adjustMaxHeap(int[] arr, int k, int size) {
int leftChild = (k << 1) + 1;
int rightChild = (k << 1) + 2;
if (leftChild < size) {
int max = k;
if (arr[leftChild] > arr[max]) {
max = leftChild;
}
if (rightChild < size && arr[rightChild] > arr[max]) {
max = rightChild;
}
if (max != k) {
//改变了左孩子或者右孩子,递归调整
int tmp = arr[max];
arr[max] = arr[k];
arr[k] = tmp;
adjustMaxHeap(arr, max, size);
}
}
}
/**
* 创建最大堆,从底往上调整
* @param arr
*/
private static void buildMaxHeap(int[] arr) {
//从最后一个非叶子节点开始调整
for (int i = arr.length/2-1; i >= 0; i--) {
adjustMaxHeap(arr, i, arr.length);
}
}
/**
* 排序前首先构建最大堆,然后将堆顶和最后一个位置的数据交换,重新调整位置0处的堆
* @param arr
*/
private static void sort(int[] arr) {
buildMaxHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
int tmp = arr[i];
arr[i] = arr[0];
arr[0] = tmp;
//调整堆的时候,去掉最后一项,size应减1
adjustMaxHeap(arr, 0, i);
}
}
public static void main(String[] args) {
int[] arr = {4,8,5,3,13,11,9,2,7,1};
sort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + ", ");
}
}
}
复杂度
时间复杂度:O(nlogn)
空间复杂度:O(1)