文章目录
前言
堆结构、堆排序及其应用
一、堆结构
堆结构是一个数组对象,就是一颗特殊的完全二叉树或是一颗特殊的满二叉树。
堆分为大根堆和小根堆。
满二叉树:
完全二叉树:()
某个节点i
- 左孩子节点下标索引:i*2+1
- 右孩子节点下标索引:i*2+2
- 父节点下标索引:(i-1)/2
1、大根堆
每一棵子树的最大值为头节点的值,就为大根堆
如下
(1)向上调整(heapInsert)
调整某个位置能否往上移动
代码如下:
// 某个数在index位置能否往上移动
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;
}
}
(2)向下调整(heapify)
某个位置能否往下移动。
返回最大值并移除:最大值就是第一个值,用最后边那个数代替头节点,整体堆长度减小一,此时需要对这个头节点进行调整,在子孩子中找到最大的再与头节点比较,如果头节点小于子孩子就交换位置,以此类推当子孩子不大于头结点或者没有子孩子时,停止,此时为大根堆。
调整代价为:O(logN)
代码如下:
// 某个数在Index位置,能否往下移动
public static void heapify(int[]arr,int index,int 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,index,largest);
index=largest;
left=index*2+1;
}
}
(3)合并应用
当改变其中某个值,可以分别使用向下调整和向上调整,调整过后得到大根堆。
调整代价为:O(logN)
2、小根堆
每一棵子树的最小值为头节点的值,就为小根堆,和大根堆刚好相反。
如下
二、堆排序
1.过程
不断的从0~1,0 ~2…做到大根堆或小根堆,heapSize为堆长度,每一次新进来个数heapSize++,最终得到堆,然后将最大的与最后一个位置交换,heapSize–,就将最大位置排在了最后并剔除了堆,利用向下调整得到堆,再重复上述步骤,最终heapSize为0时,这个数组就有序了。
时间复杂度为:O(NlogN)
额外空间复杂度为:O(1)
代码如下:
public class bigRootPile {
public static void main(String[] args) {
int []arr={2,4,5,1,8,2,9,4,3,6};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[]arr){
if (arr==null||arr.length<2){
return;
}
// O(NlogN)
// 可优化
for (int i=0;i<arr.length;i++){
heapInsert(arr,i);
}
int heapSize=arr.length;
swap(arr,0,--heapSize);
while (heapSize>0){
heapify(arr,0,heapSize);// O(logN)
swap(arr,0,--heapSize);//O(1)
}
}
// 某个数在index位置能否往上移动
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;
}
}
// 某个数在Index位置,能否往下移动
public static void heapify(int[]arr,int index,int 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,index,largest);
index=largest;
left=index*2+1;
}
}
// 交换
public static void swap(int[]arr,int index,int fIndex){
int temp=arr[index];
arr[index]=arr[fIndex];
arr[fIndex]=temp;
}
}
结果如下:
只让一个不为大根堆的变为大根堆可以将以下代码优化
for (int i=0;i<arr.length;i++){
heapInsert(arr,i);
}
优化后:
for (int i=arr.length-1;i>=0;i--){
heapify(arr,i,arr.length);
}
在最后一个数往下调整,做到依次局部大根堆,最终得到大根堆。
三、堆排序扩展
如果做到高效的进行堆结构的调整,就必须自己手写,而不用系统自带的堆结构,因为系统自带的堆结构在调整时,是从头开始扫描调整
JAVA中的堆结构:(默认为小根堆)
PraiorityQyueueheap=new PriorityQueue<>();
(1)几乎有序数组排序
已知一个几乎有序数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对则个数据进行排序。
时间复杂度:O(NlogK)
代码如下:
public class almostOrdinal {
public static void main(String[] args) {
int k=2;
int []arr={2,8,6,4,14,12,16,16,18};
System.out.println("排序前:"+Arrays.toString(arr));
almostOrderedSort(arr,k);
System.out.println("排序后:"+Arrays.toString(arr));
}
public static void almostOrderedSort(int[]arr,int k){
PriorityQueue<Integer>heap=new PriorityQueue<>();
int index=0;
for (;index<=Math.min(arr.length,k);index++){// 把前k+1个数放入小根堆(如果k大于数组长度则把整个数组放入)
heap.add(arr[index]);
}
int i=0;
for (;index<arr.length;index++){
heap.add(arr[index]);// 新加后一位数放到小根堆中
arr[i++]= heap.poll();//小根堆弹出最小的数放到i位置
}
while (!heap.isEmpty()){// 如果没有可加的新数字 依次弹出就行
arr[i++]= heap.poll();
}
}
}
结果如下:
四、比较器
自己定义比较策略的类,实质就是重载比较运算符。
总结
以上文章主要讲了堆(大根堆、小根堆)和堆排序的思想,以及扩展,和比较器。