建堆与堆排序详细解析

1.堆的特性

堆有大根堆和小根堆,大根堆的子树,父结点比子结点都要大,小跟堆的子树,父节点都要比子结点小。

堆有这样的特性:

  • n个元素的堆的高度是 l o g n logn logn,因为堆是一棵完全二叉树或者近似一棵完全二叉树。
  • 堆中,如果父节点的index是n,那么这个结点的左右子结点的index 分别是 2 ∗ i n d e x + 1 ​ 2*index+1​ 2index+1 2 ∗ i n d e x + 2 ​ 2*index +2​ 2index+2,并且,如果堆总共有n个元素,并且根结点的index为0,堆中最后一个有子结点的父节点的index一定是 n / / 2 − 1 ​ n//2 -1​ n//21
  • 最大堆,最大的元素在根节点,最小堆,最小的元素在根节点。
2.如何建堆

如何从一个无序的列表建起起一个大根堆或者小根堆呢?利用堆的特性,我们从最后一个有子节点的父节点开始进行调整,也就是索引值为 n / / 2 − 1 n//2 -1 n//21的结点。下面以建立小跟堆为例讲解。

  • 首先我们拿到一个根节点root,检查结点root的两个子节点谁的值更小,注意需要保持两个子节点的索引值不超过堆的长度,超过了说明没有子节点

  • 比较较小的子节点和父节点谁的值小,如果子结点的值更小,则需要交换子结点和父节点。注意!!如果这里发生了交换,那么可能会影响原来较小子结点已经形成的堆,所以我们需要继续调整堆,所以这里我们可以用递归的写法。具体看代码中,如果用非递归的写法则是,我们要一直循环,向下找到这个父节点该插入的位置!!

这是递归写法

def sink(num_list, root):
    length = len(num_list)
    if 2 * root + 1 < length:
        child = 2 * root + 2 if 2 * root +2 < length and num_list[2 * root +2] < num_list[2 * root +1] else 2 * root +1  # find the small child node
        if num_list[root] > num_list[child]:  #exchange the parent node and the child if the parent node bigger than child node 
            num_list[root], num_list[child] = num_list[child], num_list[root]
            sink(num_list, child)  # continue to adjust the new heap

def main(num_list):
    for i in range(len(num_list) // 2 -1, -1, -1):  # 
        sink(num_list, i)
    print(num_list)

非递归写法:

def sink(num_list, root):
    length = len(num_list)
    while 1: 
        if 2 * root + 1 < length:
            child = 2 * root + 2 if 2 * root +2 < length and num_list[2 * root +2] < num_list[2 * root +1] else 2 * root +1  # find the small child node
            if num_list[root] > num_list[child]:  #exchange the parent node and the child if the parent node bigger than child node 
                num_list[root], num_list[child] = num_list[child], num_list[root]
                root = child
            else:
                break
        else:
            break

def main(num_list):
    for i in range(len(num_list) // 2 -1, -1, -1): # 由底向顶遍历
        sink(num_list, i)
    print(num_list)
3.堆排序

我们调整一次堆之后,最小的元素就在数组首位,将它出数组,然后再对剩余的数再用调整算法调整一次,继续出掉最小的数组。就是堆排序了。

while num_list:
    for i in range(len(num_list)):
        sink(num_list, i)
    list.pop(0)
4.建堆算法复杂度分析

假设堆的元素数目为 n ​ n​ n,则堆高为 h = l o g n ​ h = logn​ h=logn,最底下一层的父节点最多为 2 h − 2 ​ 2^{h-2}​ 2h2个,这一层的父节点最多向下调整1次,倒数第二层父节点最多为 2 h − 3 ​ 2^{h-3}​ 2h3个,最多向下调整2次,以此类推,最顶的根节点为1个,最多向下调整h-1次。

则复杂度 S = 2 h − 2 × 1 + 2 h − 3 × 2 + 2 h − 4 × 3    +    . . . + 2 0 × ( h − 1 ) S=2^{h-2}\times1+2^{h-3}\times2+2^{h-4}\times3\;+\;...+2^0\times(h-1) S=2h2×1+2h3×2+2h4×3+...+20×(h1) ,等式两边乘以2,得到 2 S 2S 2S, 2 S 2S 2S减去 S S S,得 S = 2 h − h − 1 S=2^h-h-1 S=2hh1,把 h = l o g n h=logn h=logn带入得$ S = n-logn-1 , 所 一 次 建 堆 的 算 法 复 杂 度 为 ,所一次建堆的算法复杂度为 ,O(n)$。

5.堆排序算法复杂度

以下是我的直觉猜测,没有进行数学推导。

事实上建好堆以后,每次将元素出数组,然后再调整堆,只需要进行logn次操作就可以调整完毕。总共需要n次出数组,所以算法复杂度为O(nlogn)。

你可能感兴趣的:(数据结构与算法)