数据结构——堆排序

1. 堆排序

与归并排序一样,但不同于插入排序的是,堆排序的时间复杂度是 O ( n l g n ) O(nlgn) O(nlgn)。而与插入排序相同,但不同于归并排序的是,堆排序同样具有空间原址性(原址性的定义看下面的黑体字):任何时候都只要常数个额外的元素空间存储临时数据。因此,堆排序集合了插入排序和归并排序的两种排序算法的优点。

  • 如果输入数组中仅有常数个元素需要在排序过程中存储在数组之外,则称排序算法是原址的。

堆排序引入了一种算法设计技巧:使用一种称为“堆”的数据结构来进行信息管理。堆不仅用在堆排序中,而且可以构造一种有效的优先队列。

在这里插入图片描述

如上图所示,(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树(若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树)。

树上的每一个结点对应数组中的一个元素,除了最底层,该树是完全充满的,而且是从左向右填充。表示堆的数组A包括两个属性:

  • A.length(通常)给出数组元素的个数;
  • A.heap-size表示有多少个堆元素存储在该数组中;

也就是说,虽然A[1…A.length]可能都存有数据,但只有A[1…A.heap-size]中存放的是堆的有效元素,这是,0 ≤ \leq A.heap-size ≤ \leq A.length。树的根节点是A[1],这样给定一个结点的下标i,我们很容易计算得到它的父结点、左孩子和右孩子的下标:

PARENT(i)
    return i/2(向下取整)

LEFT(i)
    return 2i

RIGHT(i)
    return 2i+1

二叉堆可以分为两种形式:最大堆和最小堆。在这两种堆中,结点的值都要满足堆的性质。在最大堆中,最大堆性质是指除了根以外的所有结点 i i i都要满足: A [ P A R E N T ( i ) ] ≥ A [ i ] A[PARENT(i)]\geq A[i] A[PARENT(i)]A[i]也就是说,某个节点的值至多与其父结点一样大。因此,堆中的最大元素存放在根结点中;并且,在任一子树中,该子树所包含的所有结点的值都不大于该子树根节点的值。

最小堆的组织方式正好相反:最小堆性质是指除了根以外的所有结点 i i i都有 A [ P A R E N T ( i ) ] ≤ A [ i ] A[PARENT(i)]\leq A[i] A[PARENT(i)]A[i]最小堆中的最小元素存放在根节点中。

在堆排序算法中,使用的是最大堆。最小堆通常用于构造优先队列。如果把堆看成是一棵树,我们定义一个堆中的结点的高度为该结点到叶节点最长简单路径上边的数目;进而我们可以把堆的高度定义为根结点的高度。一个包含n个元素的堆可以看做一颗完全二叉树,那么该堆的高度是 θ ( l g n ) \theta(lgn) θ(lgn)。堆结构上的一些基本操作的运行时间至多与树的高度成正比,即时间复杂度为 O ( l g n ) O(lg n) O(lgn)

1.1 维护堆的性质

建立一个函数MAX-HEAPIFY,MAX-HEAPIFY是用于维护最大堆性质的重要过程。它的输入为一个数组A和一个下标i。

在调用MAX-HEAPIFY的时候,我们假定根结点为LEFT(i)和RIGHT(i)的二叉树都是最大堆,但这时候A[i]有可能小于其孩子,这就违背了最大堆的性质。

MAX-HEAPIFY通过让A[i]的值在最大堆中“逐级下降”,从而使得以下标i为根结点的子树重新遵循最大堆的性质。

MAX-HEAPIFY(A,i)

l=LEFT(i)
r=RIGHT(i)

if l <= A.heap-size and A[l] > A[i]
    larget = l
else largest = i

if r <= A.heap-size and A[r] > A[largest]
    largest = r
    
if largest != i
    exchange A[i] with A[largest]
    MAX_HEAPIFY(A, largest)

在程序中的每一步中,从A[i]、A[LEFT(i)]和A[RIGHT(i)]中选出最大的,并将其下标存储在largest中。如果A[i]是最大的,那么以i为根结点的子树已经是最大堆,程序结束。否则,最大元素是i的某个孩子结点,则交换A[i]和A[largest]的值。从而使i及其孩子都满足最大堆性质。在交换后,下标为largest的结点的值是原来的A[i],于是以该结点为根的子树又有可能会违反最大堆的性质。因此,需要对该子树递归调用MAX-HEAPIFY。MAX-HEAPIFY函数的时间复杂度是 O ( h ) O(h) O(h)

1.2 建堆

我们可以用自低向上的方法利用过程MAX-HEAPIFY把一个大小为n=A.length的数组A[1…n]转换为最大堆。容易知道,子数组 A ( ⌊ n / 2 ⌋ + 1.. n ) A(\left \lfloor n/2 \right \rfloor+1..n) A(n/2+1..n)中的元素都是树的叶结点。(如果n的个数刚好是2的k次方,则最后一层的数据的个数刚好就是n/2,如果数据的个数刚好是2的k次方+1,那么叶子结点的个数也符合上面的规律,其余的情况也类似)。

因此每个叶结点都可以看成只包含一个元素的堆。过程BUILD-MAX-HEAP对树中的其他结点都调用一次MAX-HEAPIFY。

BUILD-MAX-HEAP

A.heap-size = A.length
for i = A.length/2(向下取整)+1
    MAX-HEAPIFY(A,i)

我们分析一下BUILD-MAX-HEAP的时间复杂度,每次调用MAX-HEAPIFY的时间复杂度是 O ( l g n ) O(lgn) O(lgn),BUILD-MAX-HEAP需要 O ( n ) O(n) O(n)次这样的调用。因此总的时间复杂度是 O ( n l g n ) O(nlgn) O(nlgn)

1.3 堆排序算法

初始时候,堆排序算法利用BUILD-MAX-HEAP将输入数组A[1…n]建成最大堆,其中n=A.length。因为数组中的最大元素总是在根节点A[1]中,通过把它与A[n]进行互换,我们可以让该元素放到正确的位置。这时候,如果我们从堆中去掉结点n(这一操作可以通过减少A.heap-size的值来实现),剩余的结点中,原来根的孩子结点仍然是最大堆,而新的根结点可能会违背最大堆的性质。

为了维护最大堆的性质,我们要做的是调用MAX-HEAPIFY(A,1),从而在A[1…n-1]上构造一个新的最大堆,堆排序算法会不断重复这一过程,直到堆的大小从n-1降到2。其过程伪代码如下:

HEAPSORT(A)

BUILD-MAX-HEAP(A)

for i = A.length downto 2
    exchange A[1] with A[i]
    A.heap-size = A.heap-size - 1
    MAX-HEAPIFY(A,1)

HEAPSORT的时间复杂度是 O ( n l g n ) O(nlgn) O(nlgn),因为每次调用BUILD-MAX-HEAP的时间复杂度是 O ( n ) O(n) O(n),而n-1次调用MAX-HEAPIFY,每次的时间为 O ( l g n ) O(lgn) O(lgn)。同时,其空间复杂度为 O ( 1 ) O(1) O(1),只需要常数个额外的元素空间存储临时数据即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值