排序算法(六)——堆排序

排序


堆排序:

堆排序(heap sort)同样也是选择排序的一种,但它是对简单选择排序的一种改进,改进的着眼点:如何减少关键码间的比较次数。若能利用每趟比较后的结果,也就是在找出键值最小记录的同时,也找出键值较小的记录,则可减少后面的选择中所用的比较次数,从而提高整个排序过程的效率。

关于堆:
是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(称为小根堆),或每个结点的值都大于或等于其左右孩子结点的值(称为大根堆)。
大根堆
小根堆是同样一个道理。

基本思想:
首先将待排序的记录序列构造成一个堆,此时,选出了堆中所有记录的最大者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次大的记录,以此类推,直到堆中只有一个记录。

构造一个堆:
从一个无序队列建堆的过程就是一个反复筛选的过程。因此此序列就是一个完全二叉树的顺序存储,则所有的叶子结点都已经是堆(无左右孩子),所以只需从第 n/2 个记录开始(也就是最后一个记录的父节点),执行筛选:将父节点的值与左右孩子进行比较,若左孩子或者右孩子大,则将父节点和左孩子或者右孩子交换,知道筛选到根节点为止。这样一次筛选就能把最大值推送到根节点,从而筛选出最大值。

下面通过一个演示过程来理解:
构造堆
data= [28, 25, 16, 36, 18, 32]       d[0] = 28     d[1] = 25     d[2] = 16  ......d[5] = 32

过程说明: 每次筛选总是从堆的最后一个元素,这里为d[5], child = 2 * parent + 1; child 为 32结点 , parent为 16结点。若 parent无右孩子, 则child的下标与parent的下标关系就是: child = 2 * parent + 1(5 = 2*2 + 1); 若parent有右孩子, 则child 小于lastIndex(最后一个元素下标)。

最后一个结点(叶子)的序号是n,则最后一个分支结点即为结点n的双亲,其序号是n/2。

(1)从最后一个记录的父结点开始(16): 32 (左孩子)大于 16(父结点)交换;
(2)倒数第二个记录的父结点(25):   36(左孩子)大于 25(父结点)交换;
(3)倒数第三个记录的父结点(36为第二次交换后): 不交换;
(4)倒数第四个记录的符结点:  36 (左孩子) 大于 28(父结点) 交换;
(5)依次类推,直到最最大值推送到堆顶。这样一个大堆就建立好了。
	 //对数组从0到lastIndex建大顶堆
    private void buildMaxHeap(T[] t, int lastIndex){
        
        for(int i= ( lastIndex) / 2; i >= 0; i--){ //从lastIndex处节点(最后一个节点)的父节点开始 
            int parent = i;   //保存即为最后一个节点的父节点。
            
            while(parent*2 + 1 <= lastIndex){  //如果父节点有孩子(左孩子: parent*2+1)  
            	
                int child = 2 * parent + 1;    //child为 左孩子 的索引,并且记录较大孩子的索引
                
                if(child < lastIndex){  //如果 左孩子小于lastIndex,即代表parent有右孩子(位置为child+1)
                    if(compare(t[child], t[child+1])){         //如果右孩子的值较大  
                    	child++;     //child总是记录较大孩子的索引,这里右孩子比较大 
                    }  
                }  
               
                if(compare(t[parent], t[child])){   //如果父节点的值小于其较大的子节点的值  
                    swap(t, parent, child);  //交换父节点和子节点
                    parent = child;  //将child赋予parent,开始while循环的下一次循环,重新保证parent节点的值大于其左右子节点的值  
                }else{  
                    break;  
                }  
            }
        }
    }
读者可以参见代码,根据上述的构建大堆流程图进行演示一次。

处理一次构建出来大堆的堆顶记录:
建堆后仍然将整个记录分为有序区和无序区,无序区为整个堆。初始化堆就是整个记录,无序区为空。每次构建堆完成后,堆顶和堆中最后一个记录交换,则有序区增加一个记录,堆中就少了一个记录。第 i 次处理堆顶是将堆顶记录r[0]与序列中第n-i-1个记录r[n-i-1]交换。交换


重复进行重建堆以及移走堆顶记录的操作:第 i 次调整剩余记录,此时,剩余记录有n-i个,调整根结点至第n-i个记录。
	/**
	 * 选择排序: 堆排序
	 */
	@Override
	public void heapSort(T[] t) {
		int length = t.length;
        for(int i = 0; i < length-1; i++){   //循环建立大堆并移走堆顶。 
            buildMaxHeap(t, length-1-i);  //建立大堆  
            swap(t, 0, length-1-i);  //交换堆顶和堆的最后一个元素  
        }  
        print(t);
	}
	
    //交换记录
    private  void swap(T[] t, int i, int j) {  
        T tmp = t[i];  
        t[i] = t[j];  
        t[j] = tmp;  
    } 
上面的compare 、 print 方法将在讲解完全部排序介绍,这里仍然才用泛型。

性能分析:
堆排序的运行时间主要消耗在重建堆是进行的多次筛选,需要构建n-1次堆,并且第i 次构建堆需要用O(logi)的时间,所以堆排序的时间复杂度为O(NlogN),在最好、最好、平均情况下都是如此。对于原始记录的分布,堆排序并没有什么区别,这也是堆排序相对于快速排序的优点。

稳定性:
若两个记录A和B值相等,但是排序后A、B的先后次序保持不变,则这种排序是稳定的,否则就是不稳定。堆排序是一种不稳定的排序算法。

总结:
(1)堆排序需要很好的理解堆的概念以及完全二叉树的特点,对于堆的构建是一个难点。
(2)对于原始记录的分布,堆排序并没有什么区别,这也是堆排序相对于快速排序的优点。
(3)堆排序在找出最大记录的同时,也将较大的记录往堆顶移动,减少了后面的比较次数,从而提高了排序效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值