万书网 > 文学作品 > 漫画算法:小灰的算法之旅 > 4.4什么是堆排序

4.4什么是堆排序





4.4.1 传说中的堆排序

还记得二叉堆的特性是什么吗?

1.  最大堆的堆顶是整个堆中的最大元素。

2.  最小堆的堆顶是整个堆中的最小元素。

以最大堆为例,如果删除一个最大堆的堆顶(并不是完全删除,而是跟末尾的节点交换位置),经过自我调整,第2大的元素就会被交换上来,成为最大堆的新堆顶。

正如上图所示,在删除值为10的堆顶节点后,经过调整,值为9的新节点就会顶替上来;在删除值为9的堆顶节点后,经过调整,值为8的新节点就会顶替上来……

由于二叉堆的这个特性,每一次删除旧堆顶,调整后的新堆顶都是大小仅次于旧堆顶的节点。那么只要反复删除堆顶,反复调整二叉堆,所得到的集合就会成为一个有序集合,过程如下。

删除节点9,节点8成为新堆顶。

删除节点8,节点7成为新堆顶。

删除节点7,节点6成为新堆顶。

删除节点6,节点5成为新堆顶。

删除节点5,节点4成为新堆顶。

删除节点4,节点3成为新堆顶。

删除节点3,节点2成为新堆顶。

到此为止,原本的最大二叉堆已经变成了一个从小到大的有序集合。之前说过,二叉堆实际存储在数组中,数组中的元素排列如下。

由此,可以归纳出堆排序算法的步骤。

1.  把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。

2.  循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。

大体思路明白了,那么该如何用代码来实现呢?

讲二叉堆时,我们写了二叉堆操作的相关代码。现在只要在原代码的基础上稍微改动一点点,就可以实现堆排序了。



4.4.2 堆排序的代码实现

1.  /**

2.  *  “下沉”调整

3.  *  @param  array  待调整的堆

4.  *  @param  parentIndex  要“下沉”的父节点

5.  *  @param  length  堆的有效大小

6.  */

7.  public  static  void  downAdjust(int[]  array,  int  parentIndex,

int  length)  {

8.  //  temp  保存父节点值,用于最后的赋值

9.  int  temp  =  array[parentIndex];

10.  int  childIndex  =  2  *  parentIndex  +  1;

11.  while  (childIndex  <  length)  {

12.  //  如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子

13.  if  (childIndex  +  1  <  length  &&  array[childIndex  +  1]  >

array[childIndex])  {

14.  childIndex++;

15.  }

16.  //  如果父节点大于任何一个孩子的值,则直接跳出

17.  if  (temp  >=  array[childIndex])

18.  break;

19.  //无须真正交换,单向赋值即可

20.  array[parentIndex]  =  array[childIndex];

21.  parentIndex  =  childIndex;

22.  childIndex  =  2  *  childIndex  +  1;

23.  }

24.  array[parentIndex]  =  temp;

25.  }

26.

27.

28.  /**

29.  *  堆排序(升序)

30.  *  @param  array  待调整的堆

31.  */

32.  public  static  void  heapSort(int[]  array)  {

33.  //  1.  把无序数组构建成最大堆

34.  for  (int  i  =  (array.length-2)/2;  i  >=  0;  i--)  {

35.  downAdjust(array,  i,  array.length);

36.  }

37.  System.out.println(Arrays.toString(array));

38.  //  2.  循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶


39.  for  (int  i  =  array.length  -  1;  i  >  0;  i--)  {

40.  //  最后1个元素和第1个元素进行交换

41.  int  temp  =  array[i];

42.  array[i]  =  array[0];

43.  array[0]  =  temp;

44.  //  “下沉”调整最大堆

45.  downAdjust(array,  0,  i);

46.  }

47.  }

48.

49.

50.  public  static  void  main(String[]  args)  {

51.  int[]  arr  =  new  int[]  {1,3,2,6,5,7,8,9,10,0};

52.  heapSort(arr);

53.  System.out.println(Arrays.toString(arr));

54.  }

原来如此,现在明白了!那么堆排序的时间复杂度和空间复杂度各是多少呢?

毫无疑问,空间复杂度是O(1),因为并没有开辟额外的集合空间。t至于时间复杂度,我们来分析一下。

二叉堆的节点“下沉”调整(downAdjust  方法)是堆排序算法的基础,这个调节操作本身的时间复杂度在上一章讲过,是O(log  n)。

我们再来回顾一下堆排序算法的步骤。

1.  把无序数组构建成二叉堆。

2.  循环删除堆顶元素,并将该元素移到集合尾部,调整堆产生新的堆顶。

第1步,把无序数组构建成二叉堆,这一步的时间复杂度是O(n)  。

第2步,需要进行n-1次循环。每次循环调用一次downAdjust方法,所以第2步的计算规模是  (n-1)×logn  ,时间复杂度为O(nlogn)  。

两个步骤是并列关系,所以整体的时间复杂度是O(nlogn)  。

最后一个问题,从宏观上看,堆排序和快速排序相比,有什么区别和联系呢?

先说说相同点,堆排序和快速排序的平均时间复杂度都是O(nlogn)  ,并且都是不稳定排序  。至于不同点,快速排序的最坏时间复杂度是O(n  2  )  ,而堆排序的最坏时间复杂度稳定在O(nlogn)  。

此外,快速排序递归和非递归方法的平均空间复杂度都是O(logn)  ,而堆排序的空间复杂度是O(1)  。

好了,关于堆排序算法,我们就介绍到这里。感谢大家!