排序
堆排序:
堆排序(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]交换。
/**
* 选择排序: 堆排序
*/
@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)堆排序在找出最大记录的同时,也将较大的记录往堆顶移动,减少了后面的比较次数,从而提高了排序效率。