本文共 2187 字,大约阅读时间需要 7 分钟。
前言
堆排序是以堆为原型的排序。堆首先是一棵二叉树,具有以下两个性质:每个节点的值大于或者等于其左右孩子结点的值,称为大顶堆;或者每个节点的值都小于或者等于其左右孩子结点的值,称为小顶堆。从这个定义中可以发现,堆得根节点要么是最大值要么是最小值。实现堆排序的基本思想是:将待排序的序列构造成一个大顶堆或者小顶堆。此时整个堆满足根节点是最大值或者最小值。将根节点移走,并与堆数组的最后一个值进行交换,这样的话最后一个值就是最大值或者最小值了。然后将剩余n-1(假设原来总共有n个元素)未排序的序列重新构造成一个最大堆或者最小堆,再与除原来最大值之外的最后一个元素进行交换,得到次小值。如此反复进行,就得到一个排序的序列。
堆排序算法的Java实现
package com.rhwayfun.algorithm.sort;public class HeapSort2 { public void heapSort(int[] a){ for(int i = a.length/2-1; i >= 0; i--){ //建立一个最大堆 heapAdjust(a,i,a.length - 1); } for (int i = a.length - 1; i > 0; i--) { //将堆顶元素与当前未经排序的最后一个记录交换 swap(a,0,i); //将数组a中下标从0到i-1的子序列重新调整为最大堆 heapAdjust(a, 0, i - 1); } for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } } private void swap(int[] a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } private void heapAdjust(int[] a, int s, int m) { int temp = 0,j=0; //把堆顶元素保存在临时变量中 temp = a[s]; //由于s可能是0,所以需要+1。此外,如果当前结点的序号是s,那么其左孩子是2*s+1(+1是因为s可能是0) for(j = 2*s+1; j <= m; j = 2*j+1){ //找出左右孩子较大的结点的下标 if(j < m && a[j] < a[j+1]){ ++j; } if(temp >= a[j]){ break; } //把较大的孩子结点的赋给当前结点 a[s] = a[j]; //把更大孩子结点的下标赋给s s = j; } //把原来s下标位置的值赋给新的下标为s的值,这样就完成了当前结点与更大孩子结点值的交换 a[s] = temp; } public static void main(String[] args) { new HeapSort2().heapSort(new int[]{ 90,70,80,60,10,40,50,30,20}); }}
可以发现在建立最大堆的时候,i是从a.length/2开始的,为什么要从这个位置开始呢?比如有一个数组{ 90,70,80,60,10,40,50,30,20},初始情况是这样的:
一共有9个元素,那么a.length/2−1的值是3,也就是第4个元素,执行for循环,i的值从3->2->1->0变化,可以发现这几个下标的结点都是有孩子结点的元素,所以第一个for循环就很好理解了:其实就是从下往上、从右到左,将每个非终端结点当做根节点,将其和子树调整成最大堆,所以下标3->2->1->0的变化,也就是60,80,70,90结点的调整过程。
堆排序算法的时间复杂度
可以发现堆排序的主要时间主要花在建堆和重建堆时的反复筛选上。在初始建堆的时候,由于只涉及两个节点的交换操作,所以建堆的时间复杂度是O(n)。在正式排序的时候,第i次取堆顶的元素并重建堆需要花去O(logi)的时间,需要n-1次取堆顶记录,所以排序的过程的时间复杂度是O(nlogn)。而且这是最好、最坏和平均的时间复杂度,但是由于记录的交换与比较是跳跃式的,所以与归并排序不同,堆排序是一种不稳定的排序算法。
由于初始建堆的比较次数比较多,所以堆排序不适合记录数较少的情况(使用简单排序算法是一种不错的选择,没必要大材小用了)。