Skip to content

Commit 1ac17eb

Browse files
committed
完成集合 set 讲义和代码
1 parent 577f000 commit 1ac17eb

File tree

5 files changed

+280
-1
lines changed

5 files changed

+280
-1
lines changed

docs/7_哈希表/hashtable.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ class HashTable(object):
147147

148148
具体的实现和代码编写在视频里讲解。这个代码可不太好实现,稍不留神就会有错,我们还是通过编写单元测试验证代码的正确性。
149149

150+
# 思考题
151+
- Slot 在二次探查法里为什么不能直接删除?为什么我们要给它定义几个状态?
152+
150153
# 延伸阅读
151154
- 《Data Structures and Algorithms in Python》11 章 Hash Tables
152155
- 《算法导论》第三版 11 章散列表

docs/8_字典/dict.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
字典最常使用的场景就是 k,v 存储,经常用作缓存,它的 key 值是唯一的。
1212
内置库 collections.OrderDict 还保持了 key 的添加顺序,其实用我们之前实现的链表也能自己实现一个 OrderDict。
1313

14-
# 实现 dict
14+
# 实现 dict ADT
1515

1616
其实上边 HashTable 实现的三个基本方法就是我们使用字典最常用的三个基本方法, 这里我们继承一下这个类,
1717
然后实现更多 dict 支持的方法,items(), keys(), values()。不过需要注意的是,在 python2 和 python3 里这些方法
@@ -24,3 +24,29 @@ class DictADT(HashTable):
2424
```
2525

2626
视频里我们将演示如何实现这些方法,并且写单侧验证正确性。
27+
28+
# Hashable
29+
作为 dict 的 key 必须是可哈希的,也就是说不能是 list 等可变对象。不信你在 ipython 里运行如下代码:
30+
31+
```py
32+
d = dict()
33+
d[[1]] = 1
34+
# TypeError: unhashable type: 'list'
35+
```
36+
37+
我引用 python 文档里的说法,大家可以自己理解下:
38+
39+
> An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() or __cmp__() method). Hashable objects which compare equal must have the same hash value.
40+
41+
> Hashability makes an object usable as a dictionary key and a set member, because these data structures use the hash value internally.
42+
43+
> All of Python’s immutable built-in objects are hashable, while no mutable containers (such as lists or dictionaries) are. Objects which are instances of user-defined classes are hashable by default; they all compare unequal (except with themselves), and their hash value is derived from their id().
44+
45+
46+
# 思考题:
47+
- 你能在哈希表的基础上实现 dict 的其他操作吗?
48+
- 对于 python 来说,哪些内置数据类型是可哈希的呢?
49+
- 你了解 python 的 hash 函数吗?
50+
51+
# 延伸阅读
52+
阅读 python 文档关于 dict 的相关内容

docs/9_集合/set.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# 集合 set
2+
3+
这一章讲集合,实际上它的底层也是哈希表实现的,所以像实现 DictADT 一样,借助 HashTable 实现它也比较简单。
4+
5+
6+
# 集合操作
7+
集合可能最常用的就是去重,判断是否存在一个元素等,但是 set 相比 dict 有更丰富的操作,主要是数学概念上的。
8+
如果你学过《离散数学》中集合相关的概念,基本上是一致的。 python 的 set 提供了如下基本的集合操作,
9+
假设有两个集合 A,B,有以下操作:
10+
11+
- 交集: A & B,表示同时在 A 和 B 中的元素。 python 中重载 `__and__` 实现
12+
- 并集: A | B,表示在 A 或者 B 中的元素,两个集合相加。python 中重载 `__or__` 实现
13+
- 差集: A - B,表示在 A 中但是不在 B 中的元素。 python 中重载 `__sub__` 实现
14+
- 对称差: A ^ B,返回在 A 或 B 但是不在 A、B 中都出现的元素。其实就是 (A|B) - (A&B), python 中重载 `__xor__` 实现
15+
16+
这里使用的 &, |, -, ^ 在 python 内置的 set 实现中都是重载了内置的运算符。这里我们也用这种方式实现,
17+
具体实现我会在视频里演示。python 同样实现了 intersection, union, difference, symmetric_difference 这四个方法,
18+
和使用运算符的功能是一样的。
19+
20+
![](./set.png)
21+
22+
# python frozenset
23+
在 python 里还有一个 frozenset,看它的名字就知道这种也是集合,但是它的内容是无法变动的。一般我们使用
24+
它的常见就是用一个可迭代对象初始化它,然后只用来判重等操作。
25+
26+
27+
# 实现一个 set ADT
28+
如何实现一个集合的 ADT 呢,其实还是个哈希表,哈希表不是有 key 和 value 嘛,咱把 value 置为 1 不就行了。
29+
30+
```py
31+
class SetADT(HashTable):
32+
33+
def add(self, key):
34+
# 集合其实就是一个 dict,只不过我们把它的 value 设置成 1
35+
return super(SetADT, self).add(key, True)
36+
```
37+
38+
当然其它数学上的操作就麻烦点了。
39+
40+
41+
# 思考题
42+
- 你能尝试实现对称差操作吗?这里我没有实现,留给你作为练习
43+
- 你知道如何重载 python 的内置运算符吗?这里我们实现 set 的集合操作就是用到了重载,请阅读相关 python 文档。
44+
45+
46+
# 延伸阅读
47+
阅读 python 文档关于 set 的相关章节,了解 set 还有哪些操作?比如比较运算符的概念

docs/9_集合/set.png

80.5 KB
Loading

docs/9_集合/set_adt.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# 从数组和列表章复制的代码
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+
class Slot(object):
31+
"""定义一个 hash 表 数组的槽
32+
注意,一个槽有三种状态,看你能否想明白。相比链接法解决冲突,二次探查法删除一个 key 的操作稍微复杂。
33+
1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过,查找时只要找到 UNUSED 就不用再继续探查了
34+
2.使用过但是 remove 了,此时是 HashMap.EMPTY,该探查点后边的元素扔可能是有key
35+
3.槽正在使用 Slot 节点
36+
"""
37+
38+
def __init__(self, key, value):
39+
self.key, self.value = key, value
40+
41+
42+
class HashTable(object):
43+
44+
UNUSED = None # 没被使用过的槽,作为该类变量的一个单例,下边都是is 判断
45+
EMPTY = Slot(None, None) # 使用过但是被删除的槽
46+
47+
def __init__(self):
48+
self._table = Array(7)
49+
self.length = 0
50+
51+
@property
52+
def _load_factor(self):
53+
# load factor 超过 2/3 就重新分配空间
54+
return self.length / float(len(self._table))
55+
56+
def __len__(self):
57+
return self.length
58+
59+
def _hash1(self, key):
60+
""" 计算key的hash值"""
61+
return abs(hash(key)) % len(self._table)
62+
63+
def _find_slot(self, key, for_insert=False):
64+
"""_find_slot
65+
66+
:param key:
67+
:param for_insert: 是否插入,还是仅仅查询
68+
:return: slot index or None
69+
"""
70+
index = self._hash1(key)
71+
base_index = index
72+
hash_times = 1
73+
_len = len(self._table)
74+
75+
if not for_insert: # 查找是否存在 key
76+
while self._table[index] is not HashTable.UNUSED:
77+
if self._table[index] is HashTable.EMPTY:
78+
index = (index + hash_times * hash_times) % _len # 一个简单的二次方探查
79+
continue
80+
elif self._table[index].key == key:
81+
return index
82+
index = (index + hash_times * hash_times) % _len
83+
hash_times += 1
84+
return None
85+
else:
86+
while not self._slot_can_insert(index): # 循环直到找到一个可以插入的槽
87+
index = (index + hash_times * hash_times) % _len
88+
hash_times += 1
89+
return index
90+
91+
def _slot_can_insert(self, index):
92+
return (self._table[index] is HashTable.EMPTY or self._table[index] is HashTable.UNUSED)
93+
94+
def __contains__(self, key): # in operator
95+
index = self._find_slot(key, for_insert=False)
96+
return index is not None
97+
98+
def add(self, key, value):
99+
if key in self: # key 相同值不一样的时候,用新的值
100+
index = self._find_slot(key, for_insert=False)
101+
self._table[index].value = value
102+
return False
103+
else:
104+
index = self._find_slot(key, for_insert=True)
105+
self._table[index] = Slot(key, value)
106+
self.length += 1
107+
if self._load_factor >= 0.8: # 注意超过了 阈值 rehashing
108+
self._rehash()
109+
return True
110+
111+
def _rehash(self):
112+
old_table = self._table
113+
newsize = len(self._table) * 2 + 1 # 扩大 2*n + 1
114+
self._table = Array(newsize)
115+
116+
self.length = 0
117+
118+
for slot in old_table:
119+
if slot is not HashTable.UNUSED and slot is not HashTable.EMPTY:
120+
index = self._find_slot(slot.key, for_insert=True)
121+
self._table[index] = slot
122+
self.length += 1
123+
124+
def get(self, key, default=None):
125+
index = self._find_slot(key, for_insert=False)
126+
if index is None:
127+
return default
128+
else:
129+
return self._table[index].value
130+
131+
def remove(self, key):
132+
assert key in self, 'keyerror'
133+
index = self._find_slot(key, for_insert=False)
134+
value = self._table[index].value
135+
self.length -= 1
136+
self._table[index] = HashTable.EMPTY
137+
return value
138+
139+
def __iter__(self):
140+
for slot in self._table:
141+
if slot not in (HashTable.EMPTY, HashTable.UNUSED):
142+
yield slot.key # 和 python dict 一样,默认遍历 key,需要value 的话写个 items() 方法
143+
144+
145+
#########################################
146+
# 上边是从 哈希表章 拷贝过来的代码,我们会直接继承 HashTable 实现 集合 set
147+
#########################################
148+
149+
class SetADT(HashTable):
150+
151+
def add(self, key):
152+
# 集合其实就是一个 dict,只不过我们把它的 value 设置成 1
153+
return super(SetADT, self).add(key, True)
154+
155+
def __and__(self, other_set):
156+
"""交集 A&B"""
157+
new_set = SetADT()
158+
for element_a in self:
159+
if element_a in other_set:
160+
new_set.add(element_a)
161+
for element_b in other_set:
162+
if element_b in self:
163+
new_set.add(element_b)
164+
return new_set
165+
166+
def __sub__(self, other_set):
167+
"""差集 A-B"""
168+
new_set = SetADT()
169+
new_set = SetADT()
170+
for element_a in self:
171+
if element_a not in other_set:
172+
new_set.add(element_a)
173+
return new_set
174+
175+
def __or__(self, other_set):
176+
"""并集 A|B"""
177+
new_set = SetADT()
178+
for element_a in self:
179+
new_set.add(element_a)
180+
for element_b in other_set:
181+
new_set.add(element_b)
182+
return new_set
183+
184+
185+
def test_set_adt():
186+
sa = SetADT()
187+
sa.add(1)
188+
sa.add(2)
189+
sa.add(3)
190+
assert 1 in sa # 测试 __contains__ 方法,实现了 add 和 __contains__,集合最基本的功能就实现啦
191+
192+
sb = SetADT()
193+
sb.add(3)
194+
sb.add(4)
195+
sb.add(5)
196+
197+
sorted(list(sa & sb)) == [3]
198+
sorted(list(sa - sb)) == [1, 2]
199+
sorted(list(sa | sb)) == [1, 2, 3, 4, 5]
200+
201+
202+
if __name__ == '__main__':
203+
test_set_adt()

0 commit comments

Comments
 (0)