算法相关——Java排序算法之堆排序(七)

来源:互联网 发布:工业设计软件分类 编辑:程序博客网 时间:2024/06/04 00:49

0. 前言

本系列文章将介绍一些常用的排序算法。排序是一个非常常见的应用场景,也是开发岗位面试必问的一道面试题,有人说,如果一个企业招聘开发人员的题目中没有排序算法题,那说明这个企业不是一个正规的企业,哈哈,虽然有点戏谑,但是也从侧面证明了排序算法的重要性。

本文将介绍的是常见排序算法中的堆排序


堆排序

7.1  基本思想

7.1.1  堆的概念

堆是一种特殊形式的完全二叉树,堆又分为最大堆和最小堆。最大/小堆指每个节点的值均不大于/小于其父节点的值(如果有父节点的话)。因此最大/小堆的根节点值一定是最大/小的。堆有几种基本操作必须掌握:

1堆的创建。可以声明一个容量足够的数组来存放堆内数据,下标为0的位置存放堆中的元素数。详情可见7.2代码实现中Heap的构造方法。

2堆的插入。堆的插入存在堆满、堆空、和其他,三种情况。堆满则不能再插入,这里以最大堆为例,堆空则直接插入到数组下标为1的位置作为根节点。其他的情况呢,就是先把要插入的数value放在数组最后面,再和其父节点的值相比较,若比父节点还要大,则父节点的值下移,直到value比父节点的父节点(等等若干个父节点)的值小了,或者value成了根节点,则退出循环,最后插入value,整个插入过程结束。详情可见7.2代码实现中Heapinsert()方法。

3堆的删除。堆的删除并不是指定一个元素删除,而是删除该堆此时的根节点,即最大堆的最大值lastRoot。这时,再把数组的最后一个元素lastValue,即二叉树中最下排最右边的元素lastValue放在根节点的位置,这时因为不知道该值是否配做“老大”,接下来就是对堆进行重建的时候,重建的过程也很容易理解,就是让lastValue值和第二排的最大值做比较,他们仨谁大谁上来,lastValue值一直下沉,直到它到了合适的位置,或者已经成了叶子节点则退出循环,后插入lastValue,整个删除过程结束。详情可见7.2代码实现中Heapdelete()方法。该方法返回lastRoot值。

 

7.1.2  堆排序的基本思想

堆排序的过程,其实就是把所有待排的n个元素依次insert()进堆,并且进行n次delere()的过程,每次都将delete()的返回值放在数组最后的位置。最后的序列就是一个有序数列了。


7.2  代码实现

首先是Heap类:

/**@author Calvin*@blog http://blog.csdn.net/seu_calvin/article/details/58673035*@data 2017/02/28*/public class Heap {private int[] element;public Heap(int maxSize){//数组的第0位维护一个数组中有效元素的个数element = new int[maxSize+1];element[0] = 0;}public boolean isEmpty(){return element[0] == 0;}public boolean isFull(){return element[0] == element.length-1;}public void insert(int value){if(isFull()){throw new IndexOutOfBoundsException("该堆已满");}if(isEmpty()){element[1] = value;}else{//新增元素下标int position = element[0] + 1;while(position != 1 && value > element[position/2]){//若比父节点值大,则父节点下移element[position] = element[position/2];//判断是否需要继续与父节点进行大小比较//跳出循环条件:直到(1)该值不比父节点大,或者(2)该值为最大,即到element[1]位置。position/=2;}//最终把该值放在合适位置element[position] = value;}//最后记录元素数加1element[0] ++;}public int delete(){if(isEmpty()){throw new IndexOutOfBoundsException("该堆为空");}//要删除的根元素位置先赋值为最后一个有效元素的值int deleteElement = element[1];element[1] = element[element[0]];int temp = element[1];element[0]--;//重建堆int parent = 1;int child = 2;while(child <= element[0]){if(element[child] < element[child+1]){//如果右孩子更大则孩子下标加1child++;}if(temp > element[child]){//如果上来的元素比最大的孩子都大,那重建结束break;}else{//大孩子上来element[parent] = element[child];parent = child;child *= 2;//直到child > element[0]退出循环}}//移动上来的根节点最终放置的位置为parentelement[parent] = temp;//返回删除的最开始根节点return deleteElement;}public void sort(){if(isEmpty()){throw new IndexOutOfBoundsException("该堆为空");}//依次删除元素,并把delete返回的结果放在最后int size = element[0];for(int i = 0; i < size; i++){int deleteElement = delete();element[element[0]+1] = deleteElement;}//输出排序结果for(int j = 1; j < size+1; j++){System.out.println(element[j]);}}public void printAll() {for(int j = 1; j < element[0]+1; j++){System.out.println(element[j]);}}}

接着是主函数,将待排元素依次insert()进,再调用其sort()方法,该方法就是依次删除的过程,Heap中的代码逻辑已经注释的很清楚了,认真看看其实也不难。

/**@author Calvin*@blog http://blog.csdn.net/seu_calvin/article/details/58673035*@data 2017/02/28*/public class Order {          public static void main(String[] args) {          int[] array = new int[]{3,1,5,9,6,5,0,2,4,12};         Heap order = new Heap(10);         for(int i = 0; i < array.length; i++){            order.insert(array[i]);        }        order.sort();    }    }
输出结果略。


7.3  性能特点

堆排序的插入操作时间复杂度为O(logn)n次插入总时间为nO(logn),遍历删除也一样是nO(logn)。因此堆排序的时间复杂度是O(nlogn)空间复杂度为O(1),性能是非常好的。堆排序是不稳定的。因为堆排序是要建堆的,因此更适合对大量数据进行排序时使用。但是堆排比较的几乎都不是相邻元素,对cache极不友好,这也是其比较少被采用的原因。


7.4  堆排序与快排的性能比较

那么堆排序和快排哪个效率更好呢?两者时间复杂度相同,但是空间复杂度堆排序貌似更胜一筹,答案是快速排序比堆排序的效率高很多,并且随着数据规模的扩大,二者的差距不断扩大,快速排序的优势越来越明显。因此堆排比较少被使用。原因总结如下:

1)首先要明确第一点,数学上的时间复杂度不代表实际运行时的情况。

2堆排比较的几乎都不是相邻元素,对cache极不友好,堆排中比较父节点和字节点的值大小的时候,虽然计算下标会很快完成,在大规模的数据中对数组指针寻址也需要一定的时间。而快速排序只需要将数组指针移动到相邻的区域即可。

3)堆排序中删除堆顶后的将最后的元素放到堆顶,再让其自我调整。这样有很多比较将是被浪费的,因为被拿到堆顶的那个元素几乎肯定是很小/大的,而靠近堆顶的元素又几乎肯定是很小/大的,最后一个元素能留在堆顶的可能性微乎其微,而且很有可能最终再被移动到底部。


2 0
原创粉丝点击