Skip to content

Commit 8eb0fed

Browse files
Merge pull request #201 from Liam0205/notes
[notes][20_hashtable] done.
2 parents 8e13893 + 3c8fc57 commit 8eb0fed

File tree

4 files changed

+163
-128
lines changed

4 files changed

+163
-128
lines changed

notes/18_hashtable/readme.md

Lines changed: 69 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,69 @@
1-
# 散列表
2-
3-
散列表是数组的一种扩展,利用数组下标的随机访问特性。
4-
5-
## 散列思想
6-
7-
* 键/关键字/Key:用来标识一个数据
8-
* 散列函数/哈希函数/Hash:将 Key 映射到数组下标的函数
9-
* 散列值/哈希值:Key 经过散列函数得到的数值
10-
11-
![](https://static001.geekbang.org/resource/image/92/73/92c89a57e21f49d2f14f4424343a2773.jpg)
12-
13-
本质:利用散列函数将关键字映射到数组下标,而后利用数组随机访问时间复杂度为 $\Theta(1)$ 的特性快速访问。
14-
15-
## 散列函数
16-
17-
* 形式:`hash(key)`
18-
* 基本要求
19-
1. 散列值是非负整数
20-
1. 如果 `key1 == key2`,那么 `hash(key1) == hash(key2)`
21-
1. 如果 `key1 != key2`,那么 `hash(key1) != hash(key2)`
22-
23-
第 3 个要求,实际上不可能对任意的 `key1``key2` 都成立。因为通常散列函数的输出范围有限而输入范围无限。
24-
25-
## 散列冲突¡
26-
27-
* 散列冲突:`key1 != key2``hash(key1) == hash(key2)`
28-
29-
散列冲突会导致不同键值映射到散列表的同一个位置。为此,我们需要解决散列冲突带来的问题。
30-
31-
### 开放寻址法
32-
33-
如果遇到冲突,那就继续寻找下一个空闲的槽位。
34-
35-
#### 线性探测
36-
37-
插入时,如果遇到冲突,那就依次往下寻找下一个空闲的槽位。(橙色表示已被占用的槽位,黄色表示空闲槽位)
38-
39-
![](https://static001.geekbang.org/resource/image/5c/d5/5c31a3127cbc00f0c63409bbe1fbd0d5.jpg)
40-
41-
查找时,如果目标槽位上不是目标数据,则依次往下寻找;直至遇见目标数据或空槽位。
42-
43-
![](https://static001.geekbang.org/resource/image/91/ff/9126b0d33476777e7371b96e676e90ff.jpg)
44-
45-
删除时,标记为 `deleted`,而不是直接删除。
46-
47-
#### 平方探测(Quadratic probing)
48-
49-
插入时,如果遇到冲突,那就往后寻找下一个空闲的槽位,其步长为 $1^2$, $2^2$, $3^2$, $\ldots$。
50-
51-
查找时,如果目标槽位上不是目标数据,则依次往下寻找,其步长为 $1^2$, $2^2$, $3^2$, $\ldots$;直至遇见目标数据或空槽位。
52-
53-
删除时,标记为 `deleted`,而不是直接删除。
54-
55-
#### 装载因子(load factor)
56-
57-
$\text{load factor} = \frac{size()}{capacity()}$
58-
59-
### 链表法
60-
61-
所有散列值相同的 key 以链表的形式存储在同一个槽位中。
62-
63-
![](https://static001.geekbang.org/resource/image/a4/7f/a4b77d593e4cb76acb2b0689294ec17f.jpg)
64-
65-
插入时,不论是否有冲突,直接插入目标位置的链表。
66-
67-
查找时,遍历目标位置的链表来查询。
68-
69-
删除时,遍历目标位置的链表来删除。
1+
# 散列表
2+
3+
散列表是数组的一种扩展,利用数组下标的随机访问特性。
4+
5+
## 散列思想
6+
7+
* 键/关键字/Key:用来标识一个数据
8+
* 散列函数/哈希函数/Hash:将 Key 映射到数组下标的函数
9+
* 散列值/哈希值:Key 经过散列函数得到的数值
10+
11+
![](https://static001.geekbang.org/resource/image/92/73/92c89a57e21f49d2f14f4424343a2773.jpg)
12+
13+
本质:利用散列函数将关键字映射到数组下标,而后利用数组随机访问时间复杂度为 $\Theta(1)$ 的特性快速访问。
14+
15+
## 散列函数
16+
17+
* 形式:`hash(key)`
18+
* 基本要求
19+
1. 散列值是非负整数
20+
1. 如果 `key1 == key2`,那么 `hash(key1) == hash(key2)`
21+
1. 如果 `key1 != key2`,那么 `hash(key1) != hash(key2)`
22+
23+
第 3 个要求,实际上不可能对任意的 `key1``key2` 都成立。因为通常散列函数的输出范围有限而输入范围无限。
24+
25+
## 散列冲突
26+
27+
* 散列冲突:`key1 != key2``hash(key1) == hash(key2)`
28+
29+
散列冲突会导致不同键值映射到散列表的同一个位置。为此,我们需要解决散列冲突带来的问题。
30+
31+
### 开放寻址法
32+
33+
如果遇到冲突,那就继续寻找下一个空闲的槽位。
34+
35+
#### 线性探测
36+
37+
插入时,如果遇到冲突,那就依次往下寻找下一个空闲的槽位。(橙色表示已被占用的槽位,黄色表示空闲槽位)
38+
39+
![](https://static001.geekbang.org/resource/image/5c/d5/5c31a3127cbc00f0c63409bbe1fbd0d5.jpg)
40+
41+
查找时,如果目标槽位上不是目标数据,则依次往下寻找;直至遇见目标数据或空槽位。
42+
43+
![](https://static001.geekbang.org/resource/image/91/ff/9126b0d33476777e7371b96e676e90ff.jpg)
44+
45+
删除时,标记为 `deleted`,而不是直接删除。
46+
47+
#### 平方探测(Quadratic probing)
48+
49+
插入时,如果遇到冲突,那就往后寻找下一个空闲的槽位,其步长为 $1^2$, $2^2$, $3^2$, $\ldots$。
50+
51+
查找时,如果目标槽位上不是目标数据,则依次往下寻找,其步长为 $1^2$, $2^2$, $3^2$, $\ldots$;直至遇见目标数据或空槽位。
52+
53+
删除时,标记为 `deleted`,而不是直接删除。
54+
55+
#### 装载因子(load factor)
56+
57+
$\text{load factor} = \frac{size()}{capacity()}$
58+
59+
### 链表法
60+
61+
所有散列值相同的 key 以链表的形式存储在同一个槽位中。
62+
63+
![](https://static001.geekbang.org/resource/image/a4/7f/a4b77d593e4cb76acb2b0689294ec17f.jpg)
64+
65+
插入时,不论是否有冲突,直接插入目标位置的链表。
66+
67+
查找时,遍历目标位置的链表来查询。
68+
69+
删除时,遍历目标位置的链表来删除。

notes/19_hashtable/readme.md

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,59 @@
1-
# 散列表
2-
3-
核心:散列表的效率并不总是 $O(1)$,仅仅是在理论上能达到 $O(1)$。实际情况中,恶意攻击者可以通过精心构造数据,使得散列表的性能急剧下降。
4-
5-
如何设计一个工业级的散列表?
6-
7-
## 散列函数
8-
9-
* 不能过于复杂——避免散列过程耗时
10-
* 散列函数的结果要尽可能均匀——最小化散列冲突
11-
12-
## 装载因子过大怎么办
13-
14-
动态扩容。涉及到 rehash,效率可能很低。
15-
16-
![](https://static001.geekbang.org/resource/image/67/43/67d12e07a7d673a9c1d14354ad029443.jpg)
17-
18-
如何避免低效扩容?
19-
20-
——将 rehash 的步骤,均摊到每一次插入中去:
21-
22-
* 申请新的空间
23-
* 不立即使用
24-
* 每次来了新的数据,往新表插入数据
25-
* 同时,取出旧表的一个数据,插入新表
26-
27-
![](https://static001.geekbang.org/resource/image/6d/cb/6d6736f986ec4b75dabc5472965fb9cb.jpg)
28-
29-
## 解决冲突
30-
31-
开放寻址法,优点:
32-
33-
* 不需要额外空间
34-
* 有效利用 CPU 缓存
35-
* 方便序列化
36-
37-
开放寻址法,缺点:
38-
39-
* 查找、删除数据时,涉及到 `delete` 标志,相对麻烦
40-
* 冲突的代价更高
41-
* 对装载因子敏感
42-
43-
链表法,优点:
44-
45-
* 内存利用率较高——链表的优点
46-
* 对装载因子不敏感
47-
48-
链表法,缺点:
49-
50-
* 需要额外的空间(保存指针)
51-
* 对 CPU 缓存不友好
52-
53-
——将链表改造成更高效的数据结构,例如跳表、红黑树
54-
55-
## 举个栗子(JAVA 中的 HashMap)
56-
57-
* 初始大小:16
58-
* 装载因子:超过 0.75 时动态扩容
59-
* 散列冲突:优化版的链表法(当槽位冲突元素超过 8 时使用红黑树,否则使用链表)
1+
# 散列表
2+
3+
核心:散列表的效率并不总是 $O(1)$,仅仅是在理论上能达到 $O(1)$。实际情况中,恶意攻击者可以通过精心构造数据,使得散列表的性能急剧下降。
4+
5+
如何设计一个工业级的散列表?
6+
7+
## 散列函数
8+
9+
* 不能过于复杂——避免散列过程耗时
10+
* 散列函数的结果要尽可能均匀——最小化散列冲突
11+
12+
## 装载因子过大怎么办
13+
14+
动态扩容。涉及到 rehash,效率可能很低。
15+
16+
![](https://static001.geekbang.org/resource/image/67/43/67d12e07a7d673a9c1d14354ad029443.jpg)
17+
18+
如何避免低效扩容?
19+
20+
——将 rehash 的步骤,均摊到每一次插入中去:
21+
22+
* 申请新的空间
23+
* 不立即使用
24+
* 每次来了新的数据,往新表插入数据
25+
* 同时,取出旧表的一个数据,插入新表
26+
27+
![](https://static001.geekbang.org/resource/image/6d/cb/6d6736f986ec4b75dabc5472965fb9cb.jpg)
28+
29+
## 解决冲突
30+
31+
开放寻址法,优点:
32+
33+
* 不需要额外空间
34+
* 有效利用 CPU 缓存
35+
* 方便序列化
36+
37+
开放寻址法,缺点:
38+
39+
* 查找、删除数据时,涉及到 `delete` 标志,相对麻烦
40+
* 冲突的代价更高
41+
* 对装载因子敏感
42+
43+
链表法,优点:
44+
45+
* 内存利用率较高——链表的优点
46+
* 对装载因子不敏感
47+
48+
链表法,缺点:
49+
50+
* 需要额外的空间(保存指针)
51+
* 对 CPU 缓存不友好
52+
53+
——将链表改造成更高效的数据结构,例如跳表、红黑树
54+
55+
## 举个栗子(JAVA 中的 HashMap)
56+
57+
* 初始大小:16
58+
* 装载因子:超过 0.75 时动态扩容
59+
* 散列冲突:优化版的链表法(当槽位冲突元素超过 8 时使用红黑树,否则使用链表)

notes/20_hashtable/.gitkeep

Whitespace-only changes.

notes/20_hashtable/readme.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# 散列表
2+
3+
散列表和链表的组合?为什么呢?
4+
5+
* 链表:涉及查找的操作慢,不连续存储;
6+
* 顺序表:支持随机访问,连续存储。
7+
8+
散列表 + 链表:结合优点、规避缺点。
9+
10+
## 结合散列表的 LRU 缓存淘汰算法
11+
12+
缓存的操作接口:
13+
14+
* 向缓存添加数据
15+
* 从缓存删除数据
16+
* 在缓存中查找数据
17+
18+
然而——不管是添加还是删除,都涉及到查找数据。因此,单纯的链表效率低下。
19+
20+
魔改一把!
21+
22+
![](https://static001.geekbang.org/resource/image/ea/6e/eaefd5f4028cc7d4cfbb56b24ce8ae6e.jpg)
23+
24+
* `prev``next`:双向链表——LRU 的链表
25+
* `hnext`:单向链表——解决散列冲突的链表
26+
27+
操作:
28+
29+
* 在缓存中查找数据:利用散列表
30+
* 从缓存中删除数据:先利用散列表寻找数据,然后删除——改链表就好了,效率很高
31+
* 向缓存中添加数据:先利用散列表寻找数据,如果找到了,LRU 更新;如果没找到,直接添加在 LRU 链表尾部
32+
33+
## Java: LinkedHashMap
34+
35+
遍历时,按照访问顺序遍历。实现结构,与上述 LRU 的结构完全相同——只不过它不是缓存,不限制容量大小。

0 commit comments

Comments
 (0)