为什么要使用堆排序?
在N个元素中选出前M个元素,若使用普通排序算法,时间复杂度最快是O(NlogN),使用堆排序可以把时间复杂度降低到O(NlogM)。
对一个元素的操作:
普通数组:入队:O(1),出队:O(N)
排序数组:入队:O(N),出队:O(1)
堆:入队:O(logN),出队:O(logN)
堆中某个节点的值总不大于其父节点的值,堆是一棵完全二叉树(并不是层越高数值越大)底层使用数组来实现堆,因为堆是一个完全二叉树,所以根节点若从1开始标注,左孩子的索引是根节点索引的二倍,右孩子的索引是根节点索引的二倍加一
实现一个堆的插入:先把插入的数值放在数组的末尾,然后依次与父节点比较,如果大于父节点就与父节点交换。
实现堆的取出(取出最大值):将根节点元素取出,并把数组末尾元素放在根节点的位置上,与两个孩子节点中最大的进行比较,如果大于两个孩子节点,则位置不变,否则和较大的孩子节点进行交换。(交换的时间复杂度比较高,代码中shiftDown给出了使用赋值方法进行堆的调整)
public class MaxHeap { private int capacity; private int count = 0; private int[] data; public MaxHeap(int capacity){ this.capacity = capacity; data = new int[capacity+1]; } public int size(){ return count; } public boolean isEmpty(){ return count==0; } public void insert(int item){ try { data[count+1] = item; count++; shiftUp(count); } catch (Exception e) { e.printStackTrace(); } } public int pop(){ int ans = data[1]; swap(data,1,count); count--; shiftDown(1); return ans; } private void shiftUp(int index){ while(index>1&&data[index/2]<data[index]){ swap(data,index/2,index); index/=2; } } private void shiftDown(int index){ int k = data[index];//用赋值的方法保存下来要调整的数据 while(2*index<=count){ int j = 2*index;//左右孩子中比较大的 if(j+1<=count&&data[j+1]>data[j]) j+=1; /*if(data[index]>data[j]) break; swap(data,j,index);//用交换的方法,时间复杂度比较高 index = j;*/ //用赋值的方法 if(k>data[j]){ data[index] = k; break; } data[index] = data[j]; index = j; } if(k<data[index]) data[index] = k; } private void swap(int[] data,int i,int j){ int temp = data[i]; data[i] = data[j]; data[j] = temp; }}
可以看出把堆中的元素全部取出可以得到一个逆序数组,可见可以用堆实现排序。完全二叉树的第一个非叶子节点是节点的个数除以2得到的索引值可以实现以下堆的构造函数:
public MaxHeap(int[] arr){ int n = arr.length; data = new int[n+1]; this.capacity = n; for(int i = 0;i<n;i++) data[i+1] = arr[i]; count = n; for(int i = count/2;i>0;i--) shiftDown(i); }
使用以上构造函数插入数组,然后倒序赋值,实现排序
public static void heapSort1(int[] arr){ int n = arr.length; /*MaxHeap mh = new MaxHeap(n); for(int i = 0;i<n;i++) mh.insert(arr[i]);*/ MaxHeap mh = new MaxHeap(arr); for(int i = n-1;i>=0;i--) arr[i] = mh.pop(); }
将N个元素插入到一个空堆中复杂度是O(NlogN),而用以上heapify构造方法插入数据的复杂度是O(N)(首先排除了N/2个数据的操作)堆排序的性能差于快速排序和归并排序,堆不常用于排序,更常用于动态数据的维护。
以上方法都需要先把数组放在堆中,然后再取出来,占用了O(N)的空间复杂度而使用heapify和shiftDown操作可以实现原地堆排序,不占用额外的空间复杂度。即把数组的第一个最大的元素和最后一个元素交换,再对最后一个元素之前的数组进行shiftDown,再重复以上操作,直到数组有序,此时数组的索引从0开始。
已知节点索引为i,左孩子索引为2i+1,右孩子索引为2i+2,父节点索引为(i-1)/2,第一个非叶子节点索引为(count-1)/2
public static void heapSort2(int[] arr){//不使用额外空间的排序 int n = arr.length; for(int i = (n-1)/2;i>=0;i--) shiftDown(arr,n,i); for(int i = n-1;i>0;i--){ swap(arr,0,i); shiftDown(arr,i,0); } } private static void shiftDown(int[] arr, int n, int i) { while(2*i+1<n){ int j = 2*i+1; if(j+1<n&&arr[j+1]>arr[j]) j+=1; if(arr[i]>=arr[j]) break; swap(arr,i,j); i = j; } } private static void swap(int[] data,int i,int j){ int temp = data[i]; data[i] = data[j]; data[j] = temp; }
堆排序
最新推荐文章于 2023-09-21 13:21:58 发布