什么是堆
我们面临的第一个问题是:什么是堆?
堆可以定义为一颗二叉树,树的节点中包含键,并且满足两个条件:
1.该二叉树是完全二叉树.除了最后一层,树的每一层都是满的.
2.父母优势:(以最大堆为例)父节点的键大于或等于孩子节点的键
可以用数组来实现堆.
建堆,插入新元素,删除堆顶元素
自底向上构造堆
从最后一个非叶节点开始,检查该节点是否满足父母优势.如果不满足,就将该节点与最大孩子节点的键交换,然后再检查新的位置.这个过程持续到被检查节点满足父母优势为止(叶子节点自动满足父母优势).此过程从最后一个非叶节点一直持续到根节点.
最后一个非叶节点:假设堆中有n个节点,则最后一个非叶节点为(n/2)向下取整.例如,n=7,最后一个非叶节点为3.
将一个堆调整为最大堆
将以k为根的子树调整为一个堆的过程可以简单描述如下:查看k的孩子节点中是否存在比k大的,如果不存在说明以k为根的子树已经是一个最大堆了(这里存在一个假设:在调整以k为根的子树时,以k的左右孩子节点为根的子树都已经是堆了).否则,交换k和孩子节点中较大的那个(无论两个孩子节点是否都满足大于k),假设此新节点为n,接下来就是调整以n为根节点的子树了.显然,这就是重复上面的过程,那么这个过程应该持续到什么时候结束呢?答案是到k已经是叶子节点或者没有孩子节点都比它小了.
非递归方式
1 | max_heapify_iter(H[1,...,n], k) |
递归方式
1 | max_heapify_recur(A, i) |
时间复杂度:O(lgn)
建堆
非递归方式
1 | HeapBottomUp(H[1,...,n]) |
递归方式
1 | build_max_heap(A) |
自底向上建堆时间复杂度分析
自底向上方式建堆,需要调用n/2次max_headify(将一个堆调整为最大堆),每次调用max_heapify的时间复杂度为O(logk),这里的k是被调整的子树中节点的总数。接下来,计算总的时间复杂度。
包含n个元素的堆的高度为
$$
\lfloor{lgn}\rfloor
$$
高度为h的堆最多包含
$$
\lceil{\frac{n}{2^{h+1}}}\rceil
$$
个结点。在一个高度为h的结点上运行max_heapify的代价为O(h),因此建堆的总的时间复杂度为:
$$
\sum_{h=0}^{\lfloor{lgn}\rfloor}{\lceil{\frac{n}{2^{h+1}}}\rceil} O(h)= O(n \sum_{h=0}^{\lfloor{lgn}\rfloor}\frac{h}{2^h})
$$
级数求和公式:
$$
\sum_{k=0}^{\infty}{k}{x^k} = \frac{x}{(1-x)^2}
$$
将x=1/2带入级数求和公式,可知:
$$
\sum_{k=0}^{\infty}\frac{h}{2^h} = \frac{1/2}{(1 - {1/2})^2} = 2
$$
因此,建堆的时间复杂度为:
$$
O(n \sum_{h=0}^{\lfloor{lgn}\rfloor}\frac{h}{2^h}) = O(n \sum_{h=0}^{\infty}\frac{h}{2^h}) = O(n)
$$
因此,可以在线性时间内,将一个无序数组构造成一个最大堆。
向已经建好的堆中插入元素
向一个已经建好的堆中插入新元素,首先将新元素k插入到堆中最后一个叶子节点的后面。然后按照下面的方法把这个新元素k放到适当的位置上。首先拿k和它的父节点进行比较,如果k的键小于等于父节点的值,则算法停止(当前堆已经是个最大堆了)。否则交换这个元素的位置,并比较元素k和它的新父节点。这个过程一直持续到父节点的键值大于等于元素k的键值或者元素k没有了父节点(即元素k是根节点)。
显然,插入操作需要的比较次数不会多于树的高度,即向堆中插入新元素的时间复杂度为O(lgn)。
删除堆顶元素
第一步,交换根节点的元素和堆中最后一个节点的元素。
第二步,堆的规模减去1。
第三步,调用max_heapify,将以根节点为根的子树调整为最大堆。
删除需要一次交换和一次自顶向下调整堆的过程,因此,时间复杂度取决于调整堆的时间复杂度,即为O(lgn)。
堆排序
第一步,堆排序首先构造堆,为一个给定的数组构造一个堆.时间复杂度为O(n)
第二步,从堆中删除最大键,持续此动作n-1次.时间复杂度为O(nlogn)
删除最大键并调整堆的时间复杂度:O(lgn)
堆排序的时间效率是O(nlogn),但是堆排序是原址排序,不需要任何额外的存储空间. 堆排序比快速排序运行得慢,但是比归并排序快.
1 | heap_sort(A) |