堆排序是选择排序思想上的进阶,先来了解一下选择排序。
1.选择排序
选择排序的策略:
- 一个数组中,将元素分为前后两堆:已排序 + 未排序;
- 然后从未排序 序列中,去 “选择” 一个最小值的元素,把它放到前面已排序的序列中去;
- 然后不断的重复这个过程。时间复杂度显而易见:O(n2)
public class sort {
//写一个交换数组中两个元素的方法
public static void swap(int[] nums, int indexA, int indexB) {
int tmp = nums[indexA];
nums[indexA] = nums[indexB];
nums[indexB] = tmp;
}
//选择排序
public static void selectionSort(int[] nums) {
int size = nums.length;
for(int i=0; i<size; i++) { //选择的范围 (i, size-1)
int minNum = nums[i];
int minIndex = i;
for(int j=i; j<size; j++) {
if(nums[j] < minNum) {
minNum = nums[j];
minIndex = j;
}
}
swap(nums, i, minIndex);
}
}
public static void main(String[] args) {
int[] numbers = new int[] {81,94,11,96,12,35,17,95,28,58,41,75,15};
selectionSort(numbers);
for(int i=0; i<numbers.length; i++) {
System.out.println(numbers[i]);
}
}
}
2.堆排序
关于堆排序,有两种算法
堆排序算法1,策略如下:
- 首先,把原数组 A[ ] 所有的元素调整成为一个 最大堆(用数组表示,仍为 A[ ] )
- 然后,从最大堆 A[ ] 中以O(1) 的时间,找到值最大的元素,放入到一个临时数组 TmpA[ ] 末尾,并在最大堆中删除这个最大元素。
- 接着,把 A[ ] 剩余的所有元素,调整成为一个最大堆
- 重复过程 2、3,直到 A[ ] 为空,此时TmpA[ ]就是一个已经排序好的数组。
- 将TmpA[ ]的元素都导回到 A[ ]
堆排序算法2,策略如下:
- 首先,把原数组 A[ ] 所有的元素调整成为一个 最大堆(用数组表示,仍为 A[ ] )
- 将最大堆 A[ ]的根元素(即最大值元素),放到数组的末尾(交换处理)
- 继续把数组 A[ ]剩余的其他元素(除了放到末尾的元素),调整成为一个最大堆
- 重复过程2、3
从上面的两个算法中可以看到,要实现堆排序。
需要实现:
- 如何将一个数组 调整成为一个 最大/小堆
- 删除掉 最大/小 堆的根元素后,如何继续调整剩余的元素为一个最大/小堆
关于上面这两种调整方法,本质都是一样的,向上/下 过滤 元素,即可实现,以举例子说明过滤行为:
先来一个最简单的情况:如何把下面三个结点的二叉树,调整成为一个最大堆呢?
操作如下:
- 从根结点 1 开始,去和左右儿子 都比较一下,发现这三个结点中,最大的是右儿子3
- 然后根结点 和右儿子3交换。交换后:根3,左儿2,右儿1
- 完毕
从上面的过程可以看出,根结点发生了两次和它的儿子结点交换的行为,也就是发生了两次向下过滤的行为。再来看一个稍微复杂点的例子:
假如结点7的子树 和结点9的子树。已经是一个最大堆了,那如何调整,才能使整颗树是一个最大堆呢?操作如下:
- 从根结点1开始,比较1、7、9。发现右儿子9最大。1、9交换位置
- 然后元素1开始,比较1、6、4。发现左儿子6最大。1,6交换位置
- 完毕
可以看到最大堆的建立过程,把左右子树都调整成为一个最大堆,然后根结点不断的向下过滤,最后整颗树都成为一个最大堆。
再来总结一下把一个无序的数组调整成为一个最大堆策略:
-
因为堆是一个完全二叉树,因此首先让数组满足该特性,只需要元素在二叉树的中的顺序是顺序摆放
-
先让小的子树 通过向下过滤行为 变成最大堆。然后在此基础上,向下过滤子树的根结点,变成大子树的最大堆。
-
重复过程2,直到整棵树都变成最大堆
来写一下建立最大堆的算法:
public class sort1 {
//写一个交换两个元素的方法
public static void swap(int[] nums, int indexA, int indexB) {
int tmp = nums[indexA];
nums[indexA] = nums[indexB];
nums[indexB] = tmp;
}
//向下过滤算法
private static void pervDown(int[] nums, int parent, int N) { //变量N代表当前数组的长度, 用来防止超界
int temp = nums[parent];
int child = 2 * parent + 1; //先获得左儿子的下标
while(child < N) {
//如果右孩子存在 且右孩子的值大于左孩子
if(child+1<N && nums[child+1]>nums[child]) {
child++;
}//到这里,child变量已经指向了parent结点较大的一个儿子
if(temp >= nums[child])//这就不过滤了
break;
nums[parent] = nums[child];
//更新parent和 child,准备下一轮循环
parent = child;
child = 2 * parent + 1;
}
nums[parent] = temp;
}
//删除最大堆根元素,继续调整剩余元素为最大堆
private static int deleteMax(int[] nums) {
int maxItem = nums[0];
nums[0] = nums[nums.length-1];
nums[nums.length-1] = Integer.MIN_VALUE;
pervDown(nums, 0, nums.length);
return maxItem;
}
//建立最大堆算法
private static int[] buildHeap(int[] nums) {
//从完全二叉树的最后一个有儿子的结点开始,向下过滤算法
//最后一个结点是nums.length-1,它的父结点是 (nums.length-1-1)/2 ,(这里画图就可以确定)
for(int i= (nums.length-2)/2; i>=0; i--) {
pervDown(nums, i, nums.length);
}
return nums;
}
//---------------------------------------------------------------------------------------
//堆排序1(插入排序的改进)
public static void heapSort1(int[] nums) {
int size = nums.length;
int[] tmpA = new int[nums.length];
buildHeap(nums);
for(int i=nums.length-1; i>=0; i--)
tmpA[i] = deleteMax(nums);
for(int i=0; i<nums.length; i++)
nums[i] = tmpA[i];
}
//堆排序2(插入排序的改进)
public static void heapSort2(int[] nums) {
buildHeap(nums);
for(int i=nums.length-1; i>0; i--) {
swap(nums, 0, i);
pervDown(nums, 0, i);
}
}
//---------------------------------------------------------------------------------------
public static void main(String[] args) {
int[] numbers = new int[] {81,94,11,96,12,35,17,95,28,58,41,75,15};
heapSort2(numbers);
for(int i=0; i<numbers.length; i++) {
System.out.println(numbers[i]);
}
}
}
最后再来分析一下时间复杂度
算法1(需要额外数组):
- 过滤算法:pervDown,时间复杂度和树高度有关,O(log N)
- 建堆:buildHeap --> O(N)
- 第一个循环:O(N log N)
- 第二个循环:O(N)
因此总的时间复杂度为 O(N log N),需要额外的物理空间
算法2:
- 建堆:buildHeap --> O(N)
- 交换过滤(循环):O(N log N)
因此总的时间复杂度为 O(N log N)
具体的时间复杂度推导看下这篇:https://blog.csdn.net/qq_34228570/article/details/80024306