|
1 | 1 | # 堆(heap)
|
2 |
| -前面我们讲了两种使用分治和递归解决排序问题的归并排序和快速排序,本章我们开始介绍一种有用的数据结构堆(heap), 以及借助堆来实现的堆排序,相比前两种排序算法要稍难理解和实现一些。 |
| 2 | +前面我们讲了两种使用分治和递归解决排序问题的归并排序和快速排序,中间又穿插了一把树和二叉树, |
| 3 | +本章我们开始介绍另一种有用的数据结构堆(heap), 以及借助堆来实现的堆排序,相比前两种排序算法要稍难实现一些。 |
| 4 | +最后我们简单提一下 python 标准库内置的 heapq 模块。 |
3 | 5 |
|
4 |
| -# 二叉树 |
5 |
| -这里先简单讲讲树的概念,因为这里会用到它,但是我们不会详细介绍,后边的章节我们还会碰到它。树结构是一种包括节点(nodes)和边(edges)的拥有层级关系的一种结构, 它的形式和家谱树非常类似: |
6 | 6 |
|
7 |
| - |
| 7 | +# 什么是堆? |
| 8 | +堆是一种完全二叉树(请你回顾下上一章的概念),有最大堆和最小堆两种。 |
8 | 9 |
|
9 |
| -如果你了解 linux 文件结构(tree 命令),它的结构也是一棵树。 |
10 |
| -二叉树是一种简单的树,它的每个节点最多只能包含两个孩子,以下都是一些合法的二叉树: |
| 10 | +- 最大堆: 对于每个非叶子节点 V,V 的值都比它的两个孩子大,称为 最大堆特性(heap order property) |
| 11 | +最大堆里的根总是存储最大值,最小的值存储在叶节点。 |
| 12 | +- 最小堆:和最大堆相反,每个非叶子节点 V,V 的两个孩子的值都比它大。 |
11 | 13 |
|
12 |
| - |
| 14 | + |
13 | 15 |
|
14 |
| -# 什么是堆? |
15 |
| -堆是一种用数组实现的类似二叉树的数据结构,这么说可能有点懵,没讲过二叉树也先别着急,这里不会用到太多它的概念,后边我们会讲到。看一个图你就明白了: |
| 16 | +# 堆的操作 |
| 17 | +堆提供了很有限的几个操作: |
| 18 | + |
| 19 | +- 插入新的值。插入比较麻烦的就是需要维持堆的特性。需要 sift-up 操作,具体会在视频和代码里解释,文字描述起来比较麻烦。 |
| 20 | +- 获取并移除根节点的值。每次我们都可以获取最大值或者最小值。这个时候需要把底层最右边的节点值替换到 root 节点之后 |
| 21 | +执行 sift-down 操作。 |
| 22 | + |
| 23 | +# 堆的表示 |
| 24 | +上一章我们用一个节点类和二叉树类表示树,这里其实用数组就能实现堆。 |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +仔细观察下,因为完全二叉树的特性,树不会有间隙。对于数组里的一个下标 i,我们可以得到它的父亲和孩子的节点对应的下标: |
| 29 | + |
| 30 | +``` |
| 31 | +parent = int((i-1) / 2) # 取整 |
| 32 | +left = 2 * i + 1 |
| 33 | +right = 2 * i + 2 |
| 34 | +``` |
| 35 | +超出下标表示没有对应的孩子节点。 |
| 36 | + |
| 37 | +# 实现一个最大堆 |
| 38 | +我们将在视频里详细描述和编写各个操作 |
| 39 | + |
| 40 | +```py |
| 41 | +class MaxHeap(object): |
| 42 | + def __init__(self, maxsize=None): |
| 43 | + self.maxsize = maxsize |
| 44 | + self._elements = Array(maxsize) |
| 45 | + self._count = 0 |
| 46 | + |
| 47 | + def __len__(self): |
| 48 | + return self._count |
| 49 | + |
| 50 | + def add(self, value): |
| 51 | + if self._count >= self.maxsize: |
| 52 | + raise Exception('full') |
| 53 | + self._elements[self._count] = value |
| 54 | + self._count += 1 |
| 55 | + self._siftup(self._count-1) # 维持堆的特性 |
| 56 | + |
| 57 | + def _siftup(self, ndx): |
| 58 | + if ndx > 0: |
| 59 | + parent = int((ndx-1)/2) |
| 60 | + if self._elements[ndx] > self._elements[parent]: # 如果插入的值大于 parent,一直交换 |
| 61 | + self._elements[ndx], self._elements[parent] = self._elements[parent], self._elements[ndx] |
| 62 | + self._siftup(parent) # 递归 |
| 63 | + |
| 64 | + def extract(self): |
| 65 | + if self._count <= 0: |
| 66 | + raise Exception('empty') |
| 67 | + value = self._elements[0] # 保存 root 值 |
| 68 | + self._count -= 1 |
| 69 | + self._elements[0] = self._elements[self._count] # 最右下的节点放到root后siftDown |
| 70 | + self._siftdown(0) # 维持堆特性 |
| 71 | + return value |
| 72 | + |
| 73 | + def _siftdown(self, ndx): |
| 74 | + left = 2 * ndx + 1 |
| 75 | + right = 2 * ndx + 2 |
| 76 | + # determine which node contains the larger value |
| 77 | + largest = ndx |
| 78 | + if (left < self._count and # 有左孩子 |
| 79 | + self._elements[left] >= self._elements[largest] and |
| 80 | + self._elements[left] >= self._elements[right]): # 原书这个地方没写实际上找的未必是largest |
| 81 | + largest = left |
| 82 | + elif right < self._count and self._elements[right] >= self._elements[largest]: |
| 83 | + largest = right |
| 84 | + if largest != ndx: |
| 85 | + self._elements[ndx], self._elements[largest] = self._elements[largest], self._elements[ndx] |
| 86 | + self._siftdown(largest) |
| 87 | + |
| 88 | + |
| 89 | +def test_maxheap(): |
| 90 | + import random |
| 91 | + n = 5 |
| 92 | + h = MaxHeap(n) |
| 93 | + for i in range(n): |
| 94 | + h.add(i) |
| 95 | + for i in reversed(range(n)): |
| 96 | + assert i == h.extract() |
| 97 | +``` |
| 98 | + |
| 99 | +# 实现堆排序 |
| 100 | +上边我们实现了最大堆,每次我们都能 extract 一个最大的元素了,于是一个倒序排序函数就能很容易写出来了: |
| 101 | + |
| 102 | +```py |
| 103 | +def heapsort_reverse(array): |
| 104 | + length = len(array) |
| 105 | + maxheap = MaxHeap(length) |
| 106 | + for i in array: |
| 107 | + maxheap.add(i) |
| 108 | + res = [] |
| 109 | + for i in range(length): |
| 110 | + res.append(maxheap.extract()) |
| 111 | + return res |
| 112 | + |
| 113 | + |
| 114 | +def test_heapsort_reverse(): |
| 115 | + import random |
| 116 | + l = list(range(10)) |
| 117 | + random.shuffle(l) |
| 118 | + assert heapsort_reverse(l) == sorted(l, reverse=True) |
| 119 | +``` |
| 120 | + |
| 121 | +# python 里的 heapq |
| 122 | +python 其实自带了 heapq 模块,用来实现堆的相关操作,原理是类似的。请你阅读相关文档。 |
| 123 | + |
| 124 | +# 练习题 |
16 | 125 |
|
| 126 | +- 这里我用最大堆实现了一个 heapsort_reverse 函数,请你实现一个正序排序的函数。似乎不止一种方式 |
| 127 | +- 请你实现一个最小堆,你需要修改那些代码呢? |
| 128 | +- 我们实现的堆排序是 inplace 的吗,如果不是,你能改成 inplace 的吗? |
| 129 | +- 堆排序的时间复杂度是多少? siftup 和 siftdown 的时间复杂度是多少? |
17 | 130 |
|
18 |
| -# 思考题 |
19 | 131 |
|
20 | 132 | # 延伸阅读
|
21 | 133 | - 《算法导论》第 6 章 Heapsort
|
22 |
| -- 《Data Structures and Algorithms in Python》 13 章 |
| 134 | +- 《Data Structures and Algorithms in Python》 13.5 节 Heapsort |
| 135 | +- 阅读 Python heapq 模块的文档 |
0 commit comments