堆排序汇总
- 堆结构就是用数组实现的完全二叉树结构,底层结构是数组
- 大根堆:在完全二叉树中,每棵子树的最大值都在顶部
- 小根堆:在完全二叉树中,每棵子树的最小值都在顶部
- 优先队列就是堆结构
1、如何让数组变为堆结构?
- 方式一:heapInsert,将元素与父节点进行比较,如果比父节点大,则交换,直到当前元素达到根节点,或比自己的父节点小为止
public static void heapInsert(int[] arr,int index){
while(arr[index] > arr[(index-1)/2]){
swap(arr,index,(index-1)/2);
index = (index-1)/2;
}
}
for(int i = 0;i<arr.length;i++){//让数组堆化
heapInsert(arr,i);//O(logn)
}
- 方式二:heapify,将元素与其孩子中较大的一个进行比较,若子孩子大,则交换,直到没有子孩子或当前元素比子孩子大
public static void heapify(int[] arr,int index,int heapSize){//heapSize记录堆的大小,即数组大小
int left = index*2+1;
while(left < heapSize){
int largest = left+1<heapSize&&arr[left+1]>arr[left] ?left+1:left;//两个孩子中,取较大的孩子;若没有右孩子,则用子孩子去比
largest = arr[largest]>arr[index] ? largest:index;
if(largest == index) break;
swap(arr,largest,index);
index = largest;
left = index*2+1;
}
}
for(int i = arr.length-1;i>=0;i--){//用户一股脑把整个a[]给出来了,对应情景就是将数组改为大根堆,从最后一个节点开始heapify,这一步的复杂度为O(N),会比heapInsert更好
heapify(arr,i,arr.length);
}
两种方式的复杂度:
- 前提:数组已经一次性给出(不是一次给一个元素的方式)
- 当完全二叉树有N个节点时,高度为logN,因为heapify以及heapInsert每一个循环都是找元素的父节点或孩子节点,因此最多需要logN次即可完成一次调整
- 对于heapInsert来说,从arr[0]开始调整堆,总时间复杂度为O(nlogn)
- 对于heapify来说,从arr[arr.length-1]开始调整堆,完全二叉树最底层节点没有子孩子,且底层节点个数占总节点个数的近1/2,无需找孩子,复杂度1/2 * 1 * O(n),倒数第二层节点占1/4,找一次孩子,复杂度1/4 * 2 * O(n),依次类推,通过做差消项可得,总时间复杂度为O(n)
2、堆排序
思路:- 先让整个数组变成大根堆结构,建立堆
- 通过heapInsert从arr[0]开始,时间复杂度O(nlogn)
- 通过heapify从arr[arr.length-1]开始,时间复杂度O(n)
- 把堆顶的最大值和堆末尾的值交换,然后减少堆的大小,继续重复堆顶堆尾交换操作,直到堆的大小减为0
- 先让整个数组变成大根堆结构,建立堆
import java.util.Arrays;
public class HeapSort {
public void heapSort(int[] a){
if(a == null || a.length <2) return;
//for (int i = 0; i < a.length; i++) {//堆得元素一个一个给出来,从第一个节点开始heapInsert,复杂度O(nlogn)
// heapInsert(a,i);O(logn)
// }
for(int i = a.length-1;i>=0;i--){//用户一股脑把整个a[]给出来了,对应情景就是将数组改为大根堆,从最后一个节点开始heapify,这一步的复杂度为O(N),会更好
heapify(a,i,a.length);
}
int heapSize = a.length;
swap(a,0,--heapSize);//把最后一位的最大值和堆断掉联系
while(heapSize>0){
heapify(a,0,heapSize);
swap(a,0,--heapSize);
}
}
public void heapInsert(int[] a,int index){
while(a[index]>a[(index-1)/2]){
swap(a,index,(index-1)/2);
index = (index-1)/2 ;
}
}
public void heapify(int[] a,int index,int heapSize){//logN
int left = 2*index+1;
while(left < heapSize){
int largest = left+1 < heapSize && a[left] >= a[left+1]?left:left+1;//相等的时候,取左孩子
largest = a[largest] > a[index]?largest:index;
if(largest == index) {
break;
}
swap(a,index,largest);
index = largest;
left = 2*index+1;
}
}
public void swap(int[] a, int i,int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
int[] a = {1,3,5,32,2,4,2,3,3};
HeapSort hs = new HeapSort();
hs.heapSort(a);
System.out.println(Arrays.toString(a));
}
}
3、堆扩容
假设有n个数,每次扩容为当前容量的2倍,因此扩容次数为logn
堆的底层结构为数组,因此单次扩容时间复杂度O(n)
折合下来,n个数的扩容代价为O(nlogn)
单个数的扩容代价为logn
4、何时用系统自带堆,何时自己写堆
当需要改变堆内部某个节点的值,这时需要自己写,系统自带的堆会依次扫描,调整代价会很高。
自己写的堆,可以判断新值和旧值的大小,决定heapify还是heapInsert以减小代价,当新值>旧值,用heapInsert,否则用heapify
给堆一个数据,让堆弹出一个数据,可以用系统自带的堆
5、堆排序扩展题目
题目:已知一个几乎有序的数组,几乎有序是指,如果把数组排好序的话,每个元素移动的距离可以不超过K,并且K相对于数组来说比较小。选择合适排序算法对这个数据进行排序
public void sortedArrDistanceLessK(int[] arr,int k){
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index;
for(index = 0;index <= Math.min(arr.length,k);index++){//先将前K个数,放到小根堆里边,K个数之后的数 一定比第一个数值大,所以最小值必定在前K个数中
heap.add(arr[index]);
}
int i;
for(i = 0;index<arr.length;i++,index++){//加一个,弹一个
heap.add(arr[index]);
arr[i] = heap.poll();
}
while(!heap.isEmpty()){//最后将堆中的数都弹出
arr[i++] = heap.poll();
}
}