堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是一种选择排序,时间复杂度是o(n log n),本博客将使用最大堆来实现堆排序。对于堆的具体描述在本博客的这篇文章(https://blog.youkuaiyun.com/zhangjun62/article/details/82824759)已经对最大堆这种数据结构做出了介绍,故不做重复介绍,主要介绍堆排序。以下是最大堆结构具体实现过程
import cn.zjut.util.SortTestUtil;
//最大堆实现类
public class MaxHeap {
private int[] data;//声明数据存储数组,用数组来存储堆元素
private int count;//堆中元素个数
private int capacity;//堆的容量
//有参构造函数以传入容量作为初始化数组
public MaxHeap(int capacity) {
//数组容量比传入的值加一,因为内部数组下标从1开始表示堆
data = new int[capacity + 1];
count = 0;
this.capacity = capacity;
}
//有参构造函数将传入数组作heapify操作将其转化为堆
public MaxHeap(int[] arr, int n) {
data = new int[n + 1];
capacity = n;
for(int i = 0 ; i < n; i++)
data[i + 1] = arr[i];
count = n;
for(int i = count / 2; i >=1; i --)
siftDown(i);
}
//获取堆的尺寸大小
public int size() {
return count;
}
//判断堆是否为空
public boolean isEmpty() {
return count == 0;
}
//向堆中插入一个元素
public void insert(int item) {
//保证堆中还有位置可以插入新节点
assert(count + 1 <= capacity);
//将元素插入堆末尾
data[count + 1] = item;
count++;//维护count,自增1
siftUp(count);//维护堆的结构特性,需要将该元素做siftUp操作,也就是上移操作
}
//siftUp操作
private void siftUp(int k) {
//循环遍历整个堆数组,循环条件是元素索引小于根结点索引,并且父结点元素小于子结点元素
while(k > 1 && data[k / 2] < data[k]) {
//交换子结点与父结点元素
SortTestUtil.swap(data, k, k / 2);
//子结点索引等于父结点索引
k = k / 2;
}
}
//抽取最大值,对于最大堆就是抽取根结点元素
public int extractMax() {
//保证堆不是空堆
assert(count > 0);
//返回值是堆首元素,下标从1开始
int ret = data[1];
//交换堆第一个与最后一个元素
SortTestUtil.swap(data, 1, count);
//维护count,自减1
count--;
//维护堆结构,将对新的堆根结点元素做siftDown操作
siftDown(1);
return ret;
}
private void siftDown(int k) {
//循环遍历整个数组,循环条件是保证该结点左孩子存在
while(2 * k <= count) {
//获取左孩子索引
int j = 2 * k;
//寻找左孩子和右孩子最大一个元素,并且右孩子存在
if(j + 1 <= count && data[j + 1] > data[j])
j = j + 1;//如果右孩子大于左孩子,j等于右孩子结点索引
//如果如果当前元素比左右孩子中最大元素还大,则跳出循环,因为已经满足堆结构
if(data[k] > data[j])
break;
//否则交换这两个位置元素
SortTestUtil.swap(data, k, j);
//k等于最大值的索引
k = j;
}
}
}
堆排序的第一种实现方式,将数组元素依次插入堆中,然后不断抽取最大值从末尾开始往前赋值给原数组,从而达到排序的目的。以下是这种方式的排序实现过程
public class HeapSort {
public void heapSort(int[] arr, int n) {
//创建最大堆对象
MaxHeap maxHeap = new MaxHeap(n);
//将数组插入堆
for(int i = 0; i < n; i++) {
maxHeap.insert(arr[i]);
}
//抽取堆的最大值从原末尾赋值,符合升序排序的特点
for(int i = n - 1; i >= 0; i--) {
arr[i] = maxHeap.extractMax();
}
}
以下是测试代码
public class Main {
public static void main(String[] args){
int n = 100000;
System.out.println("Test for Random Array, size = " + n + ", random range [0, " + n + ']');
int[] arr1 = SortTestUtil.generateRandomArray(n, 0, n);
SortTestUtil.testSort("HeapSort", "cn.zjut.sort.HeapSort", "heapSort", arr1, n);
System.out.println("--------------------------------");
int swapTime = 100;
System.out.println("Test for Random Nearly Ordered Array, size = " + n + ", swap time = " + swapTime);
arr1 = SortTestUtil.generateNearlyOrderedArray(n, swapTime);
SortTestUtil.testSort("HeapSort", "cn.zjut.sort.HeapSort", "heapSort", arr1, n);
}
}
以下是关于以上测试结果
从结果来看这种方式实现的堆排序针对随机数组和近乎有序的数组效果不错 。堆排序还有另一种实现方式,在创建堆对象时利用构造函数传入数组进行Heapify操作,然后就是如上一种方式一样不断抽取最大值元素从数组末尾往前赋值从而达到排序要求,这种排序方式比上一种方式效率高,以下是这种方式实现过程
public class HeapSortHeapify {
public void heapSort(int[] arr, int n) {
MaxHeap maxHeap = new MaxHeap(arr, n);
for(int i = n - 1; i >= 0; i--) {
arr[i] = maxHeap.extractMax();
}
}
}
以下是针对两种实现方式的测试代码
import cn.zjut.util.SortTestUtil;
public class Main {
public static void main(String[] args){
int n = 100000;
System.out.println("Test for Random Array, size = " + n + ", random range [0, " + n + ']');
int[] arr1 = SortTestUtil.generateRandomArray(n, 0, n);
int[] arr2 = SortTestUtil.copyIntArray(arr1, n);
SortTestUtil.testSort("HeapSort", "cn.zjut.sort.HeapSort", "heapSort", arr1, n);
SortTestUtil.testSort("HeapSortHeapify", "cn.zjut.sort.HeapSortHeapify", "heapSort", arr2, n);
System.out.println("--------------------------------");
int swapTime = 100;
System.out.println("Test for Random Nearly Ordered Array, size = " + n + ", swap time = " + swapTime);
arr1 = SortTestUtil.generateNearlyOrderedArray(n, swapTime);
arr2 = SortTestUtil.copyIntArray(arr1, n);
SortTestUtil.testSort("HeapSort", "cn.zjut.sort.HeapSort", "heapSort", arr1, n);
SortTestUtil.testSort("HeapSortHeapify", "cn.zjut.sort.HeapSortHeapify", "heapSort", arr2, n);
}
}
以下是相关测试结果
从结果上看,确实是第二种方式的时间效率比第一种方式的时间效率要好。还有一种堆排序叫原地堆排序,不需要创建额外数组直接在原数组上直接堆排序,而上述两种实现方式都需要件堆空间复杂度需要额外的O(n)。思想是,其第一个元素v就是根节点(最大值),在具体排序过程中最大值应在末尾位置w,将两个值互换位置,此时最大值v在数组末尾,那么此时包含w在内(不包含末尾的v)的数组部分就不是最大堆了,将w位置的值进行Shift Down操作,剩下部分再次成为“最大堆”,最大值仍在第一个位置,那堆末尾的元素(即倒数第二个位置)与第一个元素交换位置,再进行Shift Down操作,依次类推。以下是具体实现过程
import cn.zjut.util.SortTestUtil;
public class HeapSortInsitu {
public void heapSort(int[] arr, int n) {
for (int i = (n - 1) / 2; i >= 0; i--)
siftDown(arr, n, i);
for (int i = n - 1; i >= 0; i--) {
SortTestUtil.swap(arr, 0, i);
siftDown(arr, i, 0);
}
}
private void siftDown(int[] arr, int n, int k) {
while (2 * k + 1 < n) {
int j = 2 * k + 1;
if (j + 1 < n && arr[j + 1] > arr[j])
j = j + 1;
if (arr[k] > arr[j])
break;
SortTestUtil.swap(arr, k, j);
k = j;
}
}
}
针对以上三种实现方式进行测试,测试代码如下
import cn.zjut.util.SortTestUtil;
public class Main {
public static void main(String[] args){
int n = 100000;
System.out.println("Test for Random Array, size = " + n + ", random range [0, " + n + ']');
int[] arr1 = SortTestUtil.generateRandomArray(n, 0, n);
int[] arr2 = SortTestUtil.copyIntArray(arr1, n);
int[] arr3 = SortTestUtil.copyIntArray(arr1, n);
SortTestUtil.testSort("HeapSort", "cn.zjut.sort.HeapSort", "heapSort", arr1, n);
SortTestUtil.testSort("HeapSortHeapify", "cn.zjut.sort.HeapSortHeapify", "heapSort", arr2, n);
SortTestUtil.testSort("HeapSortInsitu", "cn.zjut.sort.HeapSortInsitu", "heapSort", arr3, n);
System.out.println("--------------------------------");
int swapTime = 100;
System.out.println("Test for Random Nearly Ordered Array, size = " + n + ", swap time = " + swapTime);
arr1 = SortTestUtil.generateNearlyOrderedArray(n, swapTime);
arr2 = SortTestUtil.copyIntArray(arr1, n);
arr3 = SortTestUtil.copyIntArray(arr1, n);
SortTestUtil.testSort("HeapSort", "cn.zjut.sort.HeapSort", "heapSort", arr1, n);
SortTestUtil.testSort("HeapSortHeapify", "cn.zjut.sort.HeapSortHeapify", "heapSort", arr2, n);
SortTestUtil.testSort("HeapSortInsitu", "cn.zjut.sort.HeapSortInsitu", "heapSort", arr3, n);
}
}
以下是测试结果
从结果可知,原地堆排序与上述两种方式实现的堆排序时间效率差不多,但其对空间复杂度有优化。以上整个过程就是堆排序的所有过程。