Skip to content

Commit 7359b1c

Browse files
committed
docs: 更新文档
1 parent f76550a commit 7359b1c

File tree

12 files changed

+240
-228
lines changed

12 files changed

+240
-228
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@
3232
- [跳表](docs/跳表.md)
3333
- [散列表](docs/散列表.md)
3434
- [树和二叉树](docs/树和二叉树.md)
35+
- [](docs/堆.md)
36+
- [B+树](docs/B+树.md)
37+
- [字典树](docs/字典树.md)
3538
- [](docs/graph.md)
36-
- [](docs/heap.md)
37-
- [字典树](docs/trie.md)
3839
- [算法代码模板](docs/algorithm-template.md)
3940

4041
## 💻 刷题

assets/树.eddx

37.9 KB
Binary file not shown.

assets/算法.xmind

-52.2 KB
Binary file not shown.

assets/线性结构.eddx

8.74 KB
Binary file not shown.

docs/B+树.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# B+树
2+
3+
## 什么是 B+树
4+
5+
B+树是在二叉查找树的基础上进行了改造:树中的节点并不存储数据本身,而是只是作为索引。每个叶子节点串在一条链表上,链表中的数据是从小到大有序的。
6+
7+
8+
9+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220311092926.jpg)
10+
11+
改造之后,如果我们要求某个区间的数据。我们只需要拿区间的起始值,在树中进行查找,当查找到某个叶子节点之后,我们再顺着链表往后遍历,直到链表中的结点数据值大于区间的终止值为止。所有遍历到的数据,就是符合区间值的所有数据。
12+
13+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220311092929.jpg)
14+
15+
但是,我们要为几千万、上亿的数据构建索引,如果将索引存储在内存中,尽管内存访问的速度非常快,查询的效率非常高,但是,占用的内存会非常多。
16+
17+
比如,我们给一亿个数据构建二叉查找树索引,那索引中会包含大约 1 亿个节点,每个节点假设占用 16 个字节,那就需要大约 1GB 的内存空间。给一张表建立索引,我们需要 1GB 的内存空间。如果我们要给 10 张表建立索引,那对内存的需求是无法满足的。如何解决这个索引占用太多内存的问题呢?
18+
19+
我们可以借助时间换空间的思路,把索引存储在硬盘中,而非内存中。我们都知道,硬盘是一个非常慢速的存储设备。通常内存的访问速度是纳秒级别的,而磁盘访问的速度是毫秒级别的。读取同样大小的数据,从磁盘中读取花费的时间,是从内存中读取所花费时间的上万倍,甚至几十万倍。
20+
21+
这种将索引存储在硬盘中的方案,尽管减少了内存消耗,但是在数据查找的过程中,需要读取磁盘中的索引,因此数据查询效率就相应降低很多。
22+
23+
二叉查找树,经过改造之后,支持区间查找的功能就实现了。不过,为了节省内存,如果把树存储在硬盘中,那么每个节点的读取(或者访问),都对应一次磁盘 IO 操作。树的高度就等于每次查询数据时磁盘 IO 操作的次数。
24+
25+
我们前面讲到,比起内存读写操作,磁盘 IO 操作非常耗时,所以我们优化的重点就是尽量减少磁盘 IO 操作,也就是,尽量降低树的高度。那如何降低树的高度呢?
26+
27+
我们来看下,如果我们把索引构建成 m 叉树,高度是不是比二叉树要小呢?如图所示,给 16 个数据构建二叉树索引,树的高度是 4,查找一个数据,就需要 4 个磁盘 IO 操作(如果根节点存储在内存中,其他结点存储在磁盘中),如果对 16 个数据构建五叉树索引,那高度只有 2,查找一个数据,对应只需要 2 次磁盘操作。如果 m 叉树中的 m 是 100,那对一亿个数据构建索引,树的高度也只是 3,最多只要 3 次磁盘 IO 就能获取到数据。磁盘 IO 变少了,查找数据的效率也就提高了。
28+
29+
## 为什么需要B+树
30+
31+
关系型数据库中常用 B+ 树作为索引,这是为什么呢?
32+
33+
思考以下经典应用场景
34+
35+
- 根据某个值查找数据,比如 `select * from user where id=1234`
36+
- 根据区间值来查找某些数据,比如 `select * from user where id > 1234 and id < 2345`
37+
38+
为了提高查询效率,需要使用索引。而对于索引的性能要求,主要考察**执行效率和存储空间**。如果让你选择一种数据结构去存储索引,你会如何考虑?
39+
40+
以一些常见数据结构为例:
41+
42+
- **散列表**:散列表的查询性能很好,时间复杂度是 `O(1)`。但是,散列表不能支持按照区间快速查找数据。所以,散列表不能满足我们的需求。
43+
- **平衡二叉查找树**:尽管平衡二叉查找树查询的性能也很高,时间复杂度是 `O(logn)`。而且,对树进行中序遍历,我们还可以得到一个从小到大有序的数据序列,但这仍然不足以支持按照区间快速查找数据。
44+
- **跳表**:跳表是在链表之上加上多层索引构成的。它支持快速地插入、查找、删除数据,对应的时间复杂度是 `O(logn)`。并且,跳表也支持按照区间快速地查找数据。我们只需要定位到区间起点值对应在链表中的结点,然后从这个结点开始,顺序遍历链表,直到区间终点对应的结点为止,这期间遍历得到的数据就是满足区间值的数据。
45+
46+
实际上,数据库索引所用到的数据结构跟跳表非常相似,叫作 B+ 树。不过,它是通过二叉查找树演化过来的,而非跳表。B+树的应用场景
47+
48+
## 参考资料
49+
50+
- [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301)

docs/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
- [跳表](跳表.md)
2222
- [散列表](散列表.md)
2323
- [树和二叉树](树和二叉树.md)
24+
- [](堆.md)
25+
- [B+树](B+树.md)
26+
- [字典树](字典树.md)
2427
- [](graph.md)
25-
- [](heap.md)
26-
- [字典树](trie.md)
2728
- [算法代码模板](algorithm-template.md)
2829

2930
## 💻 刷题

docs/heap.md

Lines changed: 0 additions & 20 deletions
This file was deleted.

docs/sidebar.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
- [线性表的排序](线性表的排序.md)
88
- 非线性结构
99
- [散列表](散列表.md)
10-
- [](算法练习-树.md)
11-
- [](graph.md)
12-
- [](heap.md)
10+
- [树和二叉树](树和二叉树.md)
11+
- [](.md)
12+
- [B+树](B+树.md)
1313
- [字典树](trie.md)
14+
- [](graph.md)

docs/trie.md

Lines changed: 0 additions & 201 deletions
This file was deleted.

docs/堆.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#
2+
3+
## 什么是堆?
4+
5+
堆(Heap)是一个可以被看成近似完全二叉树的数组。
6+
7+
- **堆是一个完全二叉树**。完全二叉树要求,除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
8+
- **堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值**
9+
10+
堆可以分为大顶堆和小顶堆。
11+
12+
- 对于每个节点的值都大于等于子树中每个节点值的堆,叫作“**大顶堆**”。
13+
14+
- 对于每个节点的值都小于等于子树中每个节点值的堆,叫作“**小顶堆**”。
15+
16+
17+
## 如何实现堆
18+
19+
完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
20+
21+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220311112542.jpg)
22+
23+
24+
25+
堆常见的操作:
26+
27+
- HEAPIFY 建堆:把一个乱序的数组变成堆结构的数组,时间复杂度为 $$O(n)$$
28+
- HEAPPUSH:把一个数值放进已经是堆结构的数组中,并保持堆结构,时间复杂度为 $$O(log N)$$
29+
- HEAPPOP:从最大堆中取出最大值或从最小堆中取出最小值,并将剩余的数组保持堆结构,时间复杂度为 $$O(log N)$$
30+
- HEAPSORT:借由 HEAPFY 建堆和 HEAPPOP 堆数组进行排序,时间复杂度为$$ O(N log N)$$,空间复杂度为 $$O(1)$$
31+
32+
## 堆的应用场景
33+
34+
### 求 TOP N
35+
36+
堆结构的一个常见应用是建立优先队列(Priority Queue)。
37+
38+
求 Top K 的问题抽象成两类。一类是针对静态数据集合;另一类是针对动态数据集合
39+
40+
### 优先级队列
41+
42+
在优先级队列中,数据的出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队。
43+
44+
堆和优先级队列非常相似:往优先级队列中插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素。
45+
46+
> 参考:Java 的 `PriorityQueue`
47+
48+
### 求中位数

0 commit comments

Comments
 (0)