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

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;
		}
		
		//最后记录元素数加1
		element[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]){
				//如果右孩子更大则孩子下标加1
				child++;
			}
			if(temp > element[child]){
				//如果上来的元素比最大的孩子都大,那重建结束
				break;
			}else{
				//大孩子上来
				element[parent] = element[child];
				parent = child;
				child *= 2;//直到child > element[0]退出循环
			}
			
		}
		//移动上来的根节点最终放置的位置为parent
		element[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)堆排序中删除堆顶后的将最后的元素放到堆顶,再让其自我调整。这样有很多比较将是被浪费的,因为被拿到堆顶的那个元素几乎肯定是很小的,而靠近堆顶的元素又几乎肯定是很大的,最后一个元素能留在堆顶的可能性微乎其微,而且很有可能最终再被移动到底部。

 

你可能感兴趣的:(算法相关)