堆排序的分析与Java实现
基本原理
堆是一颗完全二叉树(非整二叉树),可以用数组来实现。一般分为最大堆和最小堆
最大堆的性质是堆中每一个节点的值都小于其父亲节点的值
最小堆的性质是堆中每一个节点的值都大于其父亲节点的值
具体关于堆的介绍,可以看我之前写过的二叉堆的数据结构。
无序数组建立堆的直接方法是从左到右遍历数组进行上浮操作。用途最广泛的操作是从右至左使用下沉操作,称为Heapify。
明白了堆的性质后,便会觉得用堆来实现排序是个很简单的工作。但是这里存在一个陷阱。
如果要实现一个从小到大排序的数组,我的第一印象是使用最小堆,从右到左遍历一遍。但是最小堆除了可以保证父亲节点大于左右子节点,并不能保证左儿子的值要小于右儿子的值。
因此直接拿一个无序数组改造成最小堆,并不能达到排序的目的。
事实上,要实现排序的功能,只需要实现一个最大堆就可以了。每次将最大堆的顶点(即索引0)与最后一位交换,再对剩下的数组中的顶点做下沉操作就可以了。这样每次都可以取到剩余数组中最大的值放到最后一位,从而得到一个从尾到头的递减序列,即从头到尾的递增序列。
代码实现
import java.util.Arrays;
import java.util.Random;
/**
* 堆排序
* 不同于二叉堆的数据结构
* 主要目的是将无序数组建立成最小堆
* 本次排序使用数组第0个位置的元素
*
* @Author Nino 2019/10/5
*/
public class HeapSort<E extends Comparable<E>> {
public void heapSort(E[] arr) {
int N = arr.length - 1;
// Heapify操作
// 对原先的数组进行最大堆化
// 需要从最后一个非叶节点开始就好了(因为叶节点没有子节点,不需要下沉)
for (int i = parent(N); i >= 0; i--) {
siftDown(arr, i, N);
}
//当需要最大堆的容量大于1的时候才进行取出最大值
while (N > 0) {
extractMax(arr, N--);
}
}
/**
* 对数组[0, N]的部分做下沉操作
* 这里的下沉是构建最大堆的下沉
* 把最大元素放在堆顶
*
* @param arr 数组
* @param index 需要下沉的位置
* @param N 下沉的终点
*/
private void siftDown(E[] arr, int index, int N) {
//当左儿子满足在N范围内才做下沉
while (leftChild(index) <= N) {
int j = leftChild(index);
//如果存在右儿子,且右儿子元素比左儿子大,则交换右儿子
if (j + 1 <= N && arr[j + 1].compareTo(arr[j]) > 0) {
j++;
}
if (arr[j].compareTo(arr[index]) <= 0) {
break;
}
swap(arr, j, index);
index = j;
}
}
/**
* 取出堆顶元素并放到索引N处
* 对[0, N-1]的堆进行下沉整理
* @return
*/
private void extractMax(E[] arr, int N) {
E e = arr[0];
swap(arr, 0, N);
siftDown(arr, 0, N - 1);
}
/**
* 获取左儿子节点
* @param index
* @return
*/
private int leftChild(int index) {
return index * 2 + 1;
}
/**
* 获取右儿子节点
* @param index
* @return
*/
private int rightChild(int index) {
return (index + 1) * 2;
}
/**
* 获取父亲节点
*
* @param index
* @return
*/
private int parent(int index) {
if (index == 0) {
return 0;
}
return (index - 1) / 2;
}
/**
* 交换数组元素
* @param arr
* @param i
* @param j
*/
private void swap(E[] arr, int i, int j) {
E e = arr[i];
arr[i] = arr[j];
arr[j] = e;
}
/**
* 测试用例
*
* @param args
*/
public static void main(String[] args) {
Random random = new Random();
Integer[] arr = new Integer[20];
for (int i = 0; i < arr.length; i++) {
arr[i] = Integer.valueOf(random.nextInt(30));
}
System.out.println(Arrays.asList(arr).toString());
new HeapSort<Integer>().heapSort(arr);
System.out.println(Arrays.asList(arr).toString());
}
}
性能分析
使用Heapify创建堆的复杂度为 O ( N ) O(N) O(N)
堆的高度为 l o g N logN logN,因此插入元素和删除元素的复杂度都为 l o g N logN logN
堆排序中要对N个节点进行下沉操作,复杂度为 N l o g N NlogN NlogN
综合来看,堆排序的时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)
堆排序是一种原地排序,没有利用额外的空间。
现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。