算法笔记-堆排
学习堆排之前先学习一下堆得数据结构,我们常用的一般分为小顶堆和大顶堆,我们以大顶堆为例:
上面从左到右分别是数组,数组转成堆得形式,其实就是一个完全二叉树,而最右边这个可以看到每个节点相对所有子节点来说都是最大的,这就是大顶堆,同理,小顶堆反之(这块不理解的可以看我之前写的一篇文章:https://www.cnblogs.com/gmt-hao/p/14417541.html),而我们这里的堆是数组实现的,数组的子节点和父节点的关系:假设父节点的下标为a, 左子节点大小为2*a+1 ,右子节点下标为2*a+2,假设任一子节点下标为b,而父节点的都是(b-1)/2,带着这些认识我们来看看堆得形成。
堆排的前提是先将数组构造成一个小顶堆或者大顶堆,我们这里还是以大顶堆为例,那么构造时就会出现两个情况:
1.当前插入数比父节点大,则需要一直往上交换,直到满足大顶堆规则
2.当前插入节点比子节点小,则需要往下将子节点中较大的数换上来
那么我们就先来实现一下这两种情况代码:
/** * 将数组中index下标的元素插入堆 */ private static void heapInsert(int[] arr,int index){ while(arr[index] > arr[(index-1)/2]){ swap(arr, index, (index-1)/2); index = (index-1)/2; }
这段代码太简单了,首先看while判断,其实就是看当前节点和父节点大小,若比父节点大则交换,并将当前节点下标index置为父节点下标,继续和父节点比,直到不比父节点大或者没有父节点为止。
再看当前节点比子节点小时:
private static void heapify(int[] arr,int index, int size){ int l = index*2 +1; //递归方式 if(l < size) { int r= l+1; int largest = r > (size -1) || arr[r] < arr[l] ? l : r; if(arr[index] < arr[largest]){ swap(arr, index, largest); heapify(arr,largest,size); } } //循环方式 while(l<size){ int r= l+1; int largest = r > (size -1) || arr[r] < arr[l] ? l : r; if(arr[index] >= arr[largest]){ break; } swap(arr, index, largest); index = largest; l = index*2 +1; } }
这里两个版本,上面的递归方式是我自己理解着去写的,但是后来看到别人的循环方式,可以减少压栈次数,而且更加优雅。其实理解起来都不难,首先找出子节点中较大的值,与自己比较大小,将较大的替换上来,继续往下递归(循环)。
那么堆排又是怎么做到的呢,其实有了heapInsert我们就可以将数组构造成堆了,那么如何利用堆来进行排序呢:
如上图所示,先将根节点(最大的)和最后一个交换,这样最后一个节点就是最大的数,再把堆大小缩小一个,再将堆构造成大顶堆(这样最大的数不会动),循环到最后一个数,则可以保证数组有序。代码如下:
public static void heapSort(int[] arr){ //数组构造大顶堆 for(int i=0;i<arr.length;i++){ heapInsert(arr,i); } int size = arr.length; //交换 for(int j=0;j<arr.length;j++){ swap(arr, 0, arr.length-j-1); heapify(arr,0,--size); } }
其实这种方式感觉和冒泡排序有点类似(都是每次找一个最大值嘛),只不过每次heapify的复杂度只有O(logN)。