1、堆
- 堆是一个完全二叉树。
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
2、堆的操作
2.1、往堆中插入一个元素
堆化(heapify)
下图是从下往上的堆化,在结尾插入一个元素22
2.2、删除堆顶元素
删除堆顶的元素之后,把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。
3、堆排序
- 借助于堆这种数据结构实现的排序算法,就叫作堆排序。时间复杂度非常稳定,是 O(nlogn),并且还是原地排序算法(所谓“原地”,就是不借助另一个数组,就在原数组上操作。)。
- 堆排序的过程大致分解成两个大的步骤:建堆和排序。
3.1、建堆
下图中,从后往前处理数组,并且每个数据都是从上往下堆化。
对下标从 n/2 开始到 1 的数据进行堆化,下标是 n/2+1 到 n 的节点是叶子节点,我们不需要堆化。实际上,对于完全二叉树来说,下标从 n/2+1 到 n 的节点都是叶子节点。
建堆的时间复杂度就是 O(n)
3.2、排序
建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。
数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。
这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。
堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。
整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。
堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn)
所以,堆排序整体的时间复杂度是 O(nlogn)。
'''建堆'''
def buildMaxHeap(a):
import math
# 对下标从n/2开始到0的数据进行堆化,因为之后的节点都是叶子节点
for i in range(math.floor(len(a)/2), -1, -1): # 下舍整数:8.7为8
heapify(a, i, len(a))
'''堆化'''
def heapify(a, i, length): # length作为参数,有助于在堆排序时改变堆的大小
# 堆的下标从0开始
left = 2*i+1
right = 2*i+2
largest = i
# 看此非叶子节点的左右孩子是否比其大,取最大的交换
if left < length and a[left] > a[largest]:
largest = left
if right < length and a[right] > a[largest]:
largest = right
if largest != i:
swap(a, i, largest)
heapify(a, largest, length) # 递归:看孩子的孩子节点
def swap(a, i, j):
a[i], a[j] = a[j], a[i]
'''堆排序'''
def heap_sort(a):
length = len(a) # 堆的下标范围[0, len(a)-1]
buildMaxHeap(a)
# 从len(a)-1到1
for i in range(len(a)-1, 0, -1):
swap(a, 0, i) # 将堆顶的元素(最大数),放在数组的末尾
length -= 1 # 堆中元素-1
heapify(a, 0, length) # 对第0位元素进行堆化
return a
if __name__ == "__main__":
a1 = [3, 5, 6, 7, 8]
a2 = [2, 2, 2, 2]
a3 = [4, 3, 2, 1]
a4 = [5, -1, 9, 3, 7, 8, 3, -2, 9]
a5 = [1]
heap_sort(a1)
print(a1)
heap_sort(a2)
print(a2)
heap_sort(a3)
print(a3)
heap_sort(a4)
print(a4)
heap_sort(a5)
print(a5)
4、小结
堆是一种完全二叉树,它最大特性:每个节点的值都大于等于(或小于等于)其子树节点的值。
堆中比较重要的两个操作是插入一个数据和删除堆顶元素。这两个操作都要用到堆化。
- 插入一个数据的时候,我们把新插入的数据放到数组的最后,然后从下往上堆化(Shift Up)
- 删除堆顶数据的时候,我们把数组中的最后一个元素放到堆顶,然后从上往下堆化(Shift Down)
- 这两个操作时间复杂度都是 O(logn)。
堆排序
- 堆排序包含两个过程,建堆和排序。我们将下标从 n/2 到 1 的节点,依次进行从上到下的堆化操作,然后就可以将数组中的数据组织成堆这种数据结构。
- 接下来,我们迭代地将堆顶的元素放到堆的末尾,并将堆的大小减一,然后再堆化,重复这个过程,直到堆中只剩下一个元素,整个数组中的数据就都有序排列了。
5、实际开发中,堆排序不如快速排序性能好的原因
① 堆排序数据访问的方式没有快速排序友好
- 对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。
- 比如,堆排序中,最重要的一个操作就是数据的堆化。比如对堆顶节点进行堆化,会依次访问数组下标是 1,2,4,8 的元素(举例),而不是像快速排序那样,局部顺序访问,所以,这样对 CPU 缓存是不友好的。
② 对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序
- 对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成的,比较和交换(或移动)。快速排序数据交换的次数不会比逆序度多。
- 但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。
参考文献