Skip to content

Commit c4d3197

Browse files
committed
完成heap
1 parent 97b461a commit c4d3197

File tree

4 files changed

+249
-11
lines changed

4 files changed

+249
-11
lines changed

docs/15_堆与堆排序/heap.png

53.7 KB
Loading
Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,135 @@
11
# 堆(heap)
2-
前面我们讲了两种使用分治和递归解决排序问题的归并排序和快速排序,本章我们开始介绍一种有用的数据结构堆(heap), 以及借助堆来实现的堆排序,相比前两种排序算法要稍难理解和实现一些。
2+
前面我们讲了两种使用分治和递归解决排序问题的归并排序和快速排序,中间又穿插了一把树和二叉树,
3+
本章我们开始介绍另一种有用的数据结构堆(heap), 以及借助堆来实现的堆排序,相比前两种排序算法要稍难实现一些。
4+
最后我们简单提一下 python 标准库内置的 heapq 模块。
35

4-
# 二叉树
5-
这里先简单讲讲树的概念,因为这里会用到它,但是我们不会详细介绍,后边的章节我们还会碰到它。树结构是一种包括节点(nodes)和边(edges)的拥有层级关系的一种结构, 它的形式和家谱树非常类似:
66

7-
![](./family_tree.png)
7+
# 什么是堆?
8+
堆是一种完全二叉树(请你回顾下上一章的概念),有最大堆和最小堆两种。
89

9-
如果你了解 linux 文件结构(tree 命令),它的结构也是一棵树。
10-
二叉树是一种简单的树,它的每个节点最多只能包含两个孩子,以下都是一些合法的二叉树:
10+
- 最大堆: 对于每个非叶子节点 V,V 的值都比它的两个孩子大,称为 最大堆特性(heap order property)
11+
最大堆里的根总是存储最大值,最小的值存储在叶节点。
12+
- 最小堆:和最大堆相反,每个非叶子节点 V,V 的两个孩子的值都比它大。
1113

12-
![](./binary_tree.png)
14+
![](./heap.png)
1315

14-
# 什么是堆?
15-
堆是一种用数组实现的类似二叉树的数据结构,这么说可能有点懵,没讲过二叉树也先别着急,这里不会用到太多它的概念,后边我们会讲到。看一个图你就明白了:
16+
# 堆的操作
17+
堆提供了很有限的几个操作:
18+
19+
- 插入新的值。插入比较麻烦的就是需要维持堆的特性。需要 sift-up 操作,具体会在视频和代码里解释,文字描述起来比较麻烦。
20+
- 获取并移除根节点的值。每次我们都可以获取最大值或者最小值。这个时候需要把底层最右边的节点值替换到 root 节点之后
21+
执行 sift-down 操作。
22+
23+
# 堆的表示
24+
上一章我们用一个节点类和二叉树类表示树,这里其实用数组就能实现堆。
25+
26+
![](heap_array.png)
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+
# 练习题
16125

126+
- 这里我用最大堆实现了一个 heapsort_reverse 函数,请你实现一个正序排序的函数。似乎不止一种方式
127+
- 请你实现一个最小堆,你需要修改那些代码呢?
128+
- 我们实现的堆排序是 inplace 的吗,如果不是,你能改成 inplace 的吗?
129+
- 堆排序的时间复杂度是多少? siftup 和 siftdown 的时间复杂度是多少?
17130

18-
# 思考题
19131

20132
# 延伸阅读
21133
- 《算法导论》第 6 章 Heapsort
22-
- 《Data Structures and Algorithms in Python》 13 章
134+
- 《Data Structures and Algorithms in Python》 13.5 节 Heapsort
135+
- 阅读 Python heapq 模块的文档
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# -*- coding:utf-8 -*-
2+
3+
# 第二章拷贝的 Array 代码
4+
5+
6+
class Array(object):
7+
8+
def __init__(self, size=32):
9+
self._size = size
10+
self._items = [None] * size
11+
12+
def __getitem__(self, index):
13+
return self._items[index]
14+
15+
def __setitem__(self, index, value):
16+
self._items[index] = value
17+
18+
def __len__(self):
19+
return self._size
20+
21+
def clear(self, value=None):
22+
for i in range(self._items):
23+
self._items[i] = value
24+
25+
def __iter__(self):
26+
for item in self._items:
27+
yield item
28+
29+
#####################################################
30+
# heap 实现
31+
#####################################################
32+
33+
34+
class MaxHeap(object):
35+
"""
36+
Heaps:
37+
完全二叉树,最大堆的非叶子节点的值都比孩子大,最小堆的非叶子结点的值都比孩子小
38+
Heap包含两个属性,order property 和 shape property(a complete binary tree),在插入
39+
一个新节点的时候,始终要保持这两个属性
40+
插入操作:保持堆属性和完全二叉树属性, sift-up 操作维持堆属性
41+
extract操作:只获取根节点数据,并把树最底层最右节点copy到根节点后,sift-down操作维持堆属性
42+
43+
用数组实现heap,从根节点开始,从上往下从左到右给每个节点编号,则根据完全二叉树的
44+
性质,给定一个节点i, 其父亲和孩子节点的编号分别是:
45+
parent = (i-1) // 2
46+
left = 2 * i + 1
47+
rgiht = 2 * i + 2
48+
使用数组实现堆一方面效率更高,节省树节点的内存占用,一方面还可以避免复杂的指针操作,减少
49+
调试难度。
50+
51+
"""
52+
53+
def __init__(self, maxsize=None):
54+
self.maxsize = maxsize
55+
self._elements = Array(maxsize)
56+
self._count = 0
57+
58+
def __len__(self):
59+
return self._count
60+
61+
def add(self, value):
62+
if self._count >= self.maxsize:
63+
raise Exception('full')
64+
self._elements[self._count] = value
65+
self._count += 1
66+
self._siftup(self._count-1) # 维持堆的特性
67+
68+
def _siftup(self, ndx):
69+
if ndx > 0:
70+
parent = int((ndx-1)/2)
71+
if self._elements[ndx] > self._elements[parent]: # 如果插入的值大于 parent,一直交换
72+
self._elements[ndx], self._elements[parent] = self._elements[parent], self._elements[ndx]
73+
self._siftup(parent) # 递归
74+
75+
def extract(self):
76+
if self._count <= 0:
77+
raise Exception('empty')
78+
value = self._elements[0] # 保存 root 值
79+
self._count -= 1
80+
self._elements[0] = self._elements[self._count] # 最右下的节点放到root后siftDown
81+
self._siftdown(0) # 维持堆特性
82+
return value
83+
84+
def _siftdown(self, ndx):
85+
left = 2 * ndx + 1
86+
right = 2 * ndx + 2
87+
# determine which node contains the larger value
88+
largest = ndx
89+
if (left < self._count and # 有左孩子
90+
self._elements[left] >= self._elements[largest] and
91+
self._elements[left] >= self._elements[right]): # 原书这个地方没写实际上找的未必是largest
92+
largest = left
93+
elif right < self._count and self._elements[right] >= self._elements[largest]:
94+
largest = right
95+
if largest != ndx:
96+
self._elements[ndx], self._elements[largest] = self._elements[largest], self._elements[ndx]
97+
self._siftdown(largest)
98+
99+
100+
def test_maxheap():
101+
import random
102+
n = 5
103+
h = MaxHeap(n)
104+
for i in range(n):
105+
h.add(i)
106+
for i in reversed(range(n)):
107+
assert i == h.extract()
108+
109+
110+
def heapsort_reverse(array):
111+
length = len(array)
112+
maxheap = MaxHeap(length)
113+
for i in array:
114+
maxheap.add(i)
115+
res = []
116+
for i in range(length):
117+
res.append(maxheap.extract())
118+
return res
119+
120+
121+
def test_heapsort_reverse():
122+
import random
123+
l = list(range(10))
124+
random.shuffle(l)
125+
assert heapsort_reverse(l) == sorted(l, reverse=True)
65.7 KB
Loading

0 commit comments

Comments
 (0)