Skip to content

Commit 1d6a57f

Browse files
committed
更新文档
1 parent b40a9d7 commit 1d6a57f

File tree

10 files changed

+85
-44
lines changed

10 files changed

+85
-44
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,14 @@
2828
- [栈和队列](docs/栈和队列.md) - 关键词:**`先进后出`****`后进先出`****`循环队列`**
2929
- [线性表的查找](docs/线性表的查找.md)
3030
- [线性表的排序](docs/线性表的排序.md)
31-
- [跳表](docs/跳表.md)
32-
- [哈希表](docs/哈希表.md)
31+
- [跳表](docs/跳表.md) - 关键词:**`多级索引`**
32+
- [哈希表](docs/哈希表.md) - 关键词:**`哈希函数`****`装载因子`****`哈希冲突`****`开放寻址法`****`拉链法`**
3333
- [树和二叉树](docs/树和二叉树.md)
3434
- [](docs/堆.md)
3535
- [B+树](docs/B+树.md)
3636
- [LSM 树](docs/LSM树.md)
3737
- [字典树](docs/字典树.md)
3838
- [](docs/图.md)
39-
- [算法代码模板](docs/algorithm-template.md)
4039

4140
## 💻 刷题
4241

assets/哈希表.eddx

33.2 KB
Binary file not shown.

assets/散列表.eddx

-16.7 KB
Binary file not shown.

assets/数据结构和算法.xmind

2.33 KB
Binary file not shown.

assets/跳表.eddx

60 KB
Binary file not shown.

docs/README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,20 @@
1313

1414
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200702071922.png)
1515

16-
- [算法概述](overview.md)
17-
- [复杂度分析](复杂度分析.md)
18-
- [数组和链表](数组和链表.md)
19-
- [栈和队列](栈和队列.md)
16+
- [数据结构和算法指南](数据结构和算法指南.md)
17+
- [复杂度分析](复杂度分析.md) - 关键词:**`时间复杂度`****`空间复杂度`****`大 O 表示法`****`复杂度量级`**
18+
- [数组和链表](数组和链表.md) - 关键词:**`线性表`****`一维数组`****`多维数组`****`随机访问`****`单链表`****`双链表`****`循环链表`**
19+
- [栈和队列](栈和队列.md) - 关键词:**`先进后出`****`后进先出`****`循环队列`**
2020
- [线性表的查找](线性表的查找.md)
2121
- [线性表的排序](线性表的排序.md)
22-
- [跳表](跳表.md)
23-
- [哈希表](哈希表.md)
22+
- [跳表](跳表.md) - 关键词:**`多级索引`**
23+
- [哈希表](哈希表.md) - 关键词:**`哈希函数`****`装载因子`****`哈希冲突`****`开放寻址法`****`拉链法`**
2424
- [树和二叉树](树和二叉树.md)
2525
- [](堆.md)
2626
- [B+树](B+树.md)
2727
- [LSM 树](LSM树.md)
2828
- [字典树](字典树.md)
2929
- [](图.md)
30-
- [算法代码模板](algorithm-template.md)
3130

3231
## 💻 刷题
3332

docs/sidebar.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
- 引言
2-
- [算法概述](overview.md)
2+
- [数据结构和算法指南](数据结构和算法指南.md)
33
- [复杂度分析](复杂度分析.md)
44
- 线性结构
55
- [数组和链表](数组和链表.md)

docs/哈希表.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,39 +72,41 @@
7272

7373
装载因子阈值需要选择得当。如果太大,会导致冲突过多;如果太小,会导致内存浪费严重。
7474

75-
#### 2.2.1. 开放寻址法
75+
### 2.2.1. 开放寻址法
7676

7777
开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。
7878

7979
**当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是 Java 中的 `ThreadLocalMap` 使用开放寻址法解决散列冲突的原因**
8080

8181
**线性探测**(Linear Probing):当我们往哈希表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
8282

83-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310110920.jpg)
83+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323200359.png)
8484

8585
对于使用线性探测法解决冲突的哈希表,删除操作稍微有些特别。我们不能单纯地把要删除的元素设置为空。这是为什么呢?在查找的时候,一旦我们通过线性探测方法,找到一个空闲位置,我们就可以认定哈希表中不存在这个数据。但是,如果这个空闲位置是我们后来删除的,就会导致原来的查找算法失效。本来存在的数据,会被认定为不存在。这个问题如何解决呢?
8686

8787
我们可以将删除的元素,特殊标记为 deleted。当线性探测查找的时候,遇到标记为 deleted 的空间,并不是停下来,而是继续往下探测。
8888

89-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310111103.jpg)
90-
9189
线性探测法其实存在很大问题。当哈希表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,我们可能需要探测整个哈希表,所以最坏情况下的时间复杂度为 O(n)。同理,在删除和查找时,也有可能会线性探测整张哈希表,才能找到要查找或者删除的数据。
9290

93-
#### 2.2.2. 链表法
91+
### 2.2.2. 链表法
9492

9593
在哈希表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
9694

9795
链表法比起开放寻址法,对大装载因子的容忍度更高。开放寻址法只能适用装载因子小于 1 的情况。接近 1 时,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。但是对于链表法来说,只要散列函数的值随机均匀,即便装载因子变成 10,也就是链表的长度变长了而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。
9896

9997
**基于链表的散列冲突处理方法比较适合存储大对象、大数据量的哈希表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表**
10098

101-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310111320.jpg)
99+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323200419.png)
102100

103101
当插入的时候,我们只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。那查找或删除操作的时间复杂度是多少呢?
104102

105103
实际上,这两个操作的时间复杂度跟链表的长度 k 成正比,也就是 O(k)。对于散列比较均匀的散列函数来说,理论上讲,k=n/m,其中 n 表示散列中数据的个数,m 表示哈希表中“槽”的个数。
106104

107-
## 3. 为什么需要哈希表
105+
### 开放寻址法 vs. 链表法
106+
107+
**开放寻址法适用于数据量比较小、装载因子小的场景**
108+
109+
**链表法适用于存储大对象、大数据量的哈希表****比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表**
108110

109111
## 4. 哈希表的应用场景
110112

@@ -124,7 +126,7 @@ Java 的 HashMap 工具类,其
124126

125127
- HashMap 默认的初始大小是 16
126128
- 最大装载因子默认是 0.75,当 HashMap 中元素个数超过 0.75\*capacity(capacity 表示哈希表的容量)的时候,就会启动扩容,每次扩容都会扩容为原来的两倍大小。
127-
- HashMap 底层采用链表法来解决冲突。即使负载因子和散列函数设计得再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响 HashMap 的性能。在 JDK1.8 版本中,对 HashMap 做了进一步优化:引入了红黑树。当链表长度太长(默认超过 8)时,链表就转换为红黑树。我们可以利用红黑树快速增删改查的特点,提高 HashMap 的性能。当红黑树结点个数少于 8 个的时候,又会将红黑树转化为链表。因为在数据量较小的情况下,红黑树要维护平衡,比起链表来,性能上的优势并不明显。
129+
- HashMap 底层采用链表法来解决冲突。即使负载因子和散列函数设计得再合理,也免不了会出现链表过长的情况,一旦出现链表过长,则会严重影响 HashMap 的性能。在 JDK1.8 版本中,对 HashMap 做了进一步优化:引入了红黑树。当链表长度太长(默认超过 8)时,链表就转换为红黑树。我们可以利用红黑树快速增删改查的特点,提高 HashMap 的性能。当红黑树结点个数少于 8 个的时候,又会将红黑树转化为链表。因为在数据量较小的情况下,红黑树要维护平衡,比起链表来,性能上的优势并不明显。
128130

129131
## 5. 练习
130132

docs/算法思路.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
## 递归
44

5+
### 使用递归的条件
6+
7+
递归需要满足的三个条件
8+
9+
- **一个问题的解可以分解为几个子问题的解**
10+
- **这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样**
11+
- **存在递归终止条件**
12+
13+
### 递归代码要警惕堆栈溢出
14+
15+
函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
16+
17+
那么,如何避免出现堆栈溢出呢?
18+
19+
我们可以通过在代码中限制递归调用的最大深度的方式来解决这个问题
20+
521
## 贪心算法
622

723
### 贪心算法思路

docs/跳表.md

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,89 @@
33
<!-- TOC depthFrom:2 depthTo:3 -->
44

55
- [1. 什么是跳表](#1-什么是跳表)
6-
- [1.1. 高效的动态插入和删除](#11-高效的动态插入和删除)
7-
- [1.2. 跳表索引动态更新](#12-跳表索引动态更新)
8-
- [2. 为什么需要跳表](#2-为什么需要跳表)
9-
- [3. 跳表的应用场景](#3-跳表的应用场景)
10-
- [4. 参考资料](#4-参考资料)
6+
- [1.1. 跳表的时间复杂度](#11-跳表的时间复杂度)
7+
- [1.2. 跳表的空间复杂度](#12-跳表的空间复杂度)
8+
- [2. 跳表的操作](#2-跳表的操作)
9+
- [2.1. 高效的动态插入和删除](#21-高效的动态插入和删除)
10+
- [2.2. 跳表索引动态更新](#22-跳表索引动态更新)
11+
- [3. 为什么需要跳表](#3-为什么需要跳表)
12+
- [4. 跳表的应用场景](#4-跳表的应用场景)
13+
- [5. 参考资料](#5-参考资料)
1114

1215
<!-- /TOC -->
1316

1417
## 1. 什么是跳表
1518

16-
只需要对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构叫作**跳表**(Skip list)
19+
对于一个有序数组,可以使用高效的二分查找法,其时间复杂度为 `O(log n)`
1720

18-
跳表是一种各方面性能都比较优秀的**动态数据结构**,可以支持快速的插入、删除、查找操作,写起来也不复杂,甚至可以替代[红黑树](https://zh.wikipedia.org/wiki/红黑树)(Red-black tree)。
21+
但是,即使是有序的链表,也只能使用低效的顺序查找,其时间复杂度为 `O(n)`
22+
23+
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323113532.png)
24+
25+
如何提高链表的查找效率呢?
26+
27+
我们可以对链表加一层索引。具体来说,可以每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作**索引****索引层**。索引节点中通过一个 down 指针,指向下一级结点。通过这样的改造,就可以支持类似二分查找的算法。我们把改造之后的数据结构叫作**跳表**(Skip list)。
28+
29+
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323155309.png)
30+
31+
随着数据的不断增长,一级索引层也变得越来越长。此时,我们可以为一级索引再增加一层索引层:二级索引层。
32+
33+
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323155346.png)
1934

20-
由于链表只支持顺序查找,所以其查找效率较低,时间复杂度是 `O(n)`
35+
随着数据的膨胀,当二级索引层也变得很长时,我们可以继续为其添加新的索引层。**这种链表加多级索引的结构,就是跳表**
2136

22-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310101420.jpg)
37+
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323114408.png)
2338

24-
跳表查询有多快?
39+
### 1.1. 跳表的时间复杂度
2540

26-
在一个具有多级索引的跳表中,第一级索引的结点个数大约就是 n/2,第二级索引的结点个数大约就是 n/4,第三级索引的结点个数大约就是 n/8,依次类推,也就是说,第 k 级索引的结点个数是第 `k-1` 级索引的结点个数的 `1/2`,那第 k 级索引结点的个数就是 `n/(2k)`
41+
在一个具有多级索引的跳表中,第一级索引的结点个数大约就是 `n/2`,第二级索引的结点个数大约就是 `n/4`,第三级索引的结点个数大约就是 `n/8`,依次类推,也就是说,第 `k` 级索引的结点个数是第 `k-1` 级索引的结点个数的 `1/2`,那第 k 级索引结点的个数就是 `n/(2k)`。所以**跳表查询数据的时间复杂度就是 `O(logn)`**
2742

28-
所以在跳表中查询任意数据的时间复杂度就是 `O(logn)`
43+
### 1.2. 跳表的空间复杂度
2944

30-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310102943.jpg)
45+
比起单纯的单链表,跳表需要存储多级索引,肯定要消耗更多的存储空间。
3146

32-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310103133.jpg)
47+
假设原始链表大小为 n,那第一级索引大约有 n/2 个结点,第二级索引大约有 n/4 个结点,以此类推,每上升一级就减少一半,直到剩下 2 个结点。如果我们把每层索引的结点数写出来,就是一个等比数列。
3348

34-
**这种链表加多级索引的结构,就是跳表**
49+
```
50+
索引节点数 = n/2 + n/4 + n/8 … + 8 + 4 + 2 = n-2
51+
```
3552

36-
### 1.1. 高效的动态插入和删除
53+
所以,跳表的空间复杂度是 `O(n)`
54+
55+
跳表的存储空间其实还有压缩空间。比如,我们增加索引节点的范围,由『每两个节点抽一个上级索引节点』改为『每五个节点抽一个上级索引节点』,可以显著节省存储空间。
56+
57+
实际上,在软件开发中,我们不必太在意索引占用的额外空间。在讲数据结构和算法时,我们习惯性地把要处理的数据看成整数,但是在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。
58+
59+
## 2. 跳表的操作
60+
61+
跳表是一种各方面性能都比较优秀的**动态数据结构**,可以支持快速的插入、删除、查找操作,写起来也不复杂,甚至可以替代[红黑树](https://zh.wikipedia.org/wiki/红黑树)(Red-black tree)。
62+
63+
### 2.1. 高效的动态插入和删除
3764

3865
跳表不仅支持查找操作,还支持动态的插入、删除操作,而且插入、删除操作的时间复杂度也是 `O(logn)`
3966

40-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310104105.jpg)
67+
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323155933.png)
4168

42-
- **插入操作**:对于纯粹的单链表,需要遍历每个结点,来找到插入的位置。但是,对于跳表来说,我们讲过查找某个结点的的时间复杂度是 O(logn),所以这里查找某个数据应该插入的位置,方法也是类似的,时间复杂度也是 O(logn)
69+
- **插入操作**:对于纯粹的单链表,需要遍历每个结点,来找到插入的位置。但是,对于跳表来说,我们讲过查找某个结点的的时间复杂度是 `O(log n)`,所以这里查找某个数据应该插入的位置,方法也是类似的,时间复杂度也是 `O(log n)`
4370
- **删除操作**:如果这个结点在索引中也有出现,我们除了要删除原始链表中的结点,还要删除索引中的。因为单链表中的删除操作需要拿到要删除结点的前驱结点,然后通过指针操作完成删除。所以在查找要删除的结点的时候,一定要获取前驱结点。当然,如果我们用的是双向链表,就不需要考虑这个问题了。
4471

45-
### 1.2. 跳表索引动态更新
72+
### 2.2. 跳表索引动态更新
4673

4774
当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表还会退化成单链表。
4875

49-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310104519.jpg)
76+
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220323161942.png)
5077

5178
如红黑树、AVL 树这样的平衡二叉树,是通过左右旋的方式保持左右子树的大小平衡,而跳表是通过随机函数来维护前面提到的“平衡性”。
5279

5380
当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?可以通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
5481

55-
![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220310104646.jpg)
56-
57-
## 2. 为什么需要跳表
82+
## 3. 为什么需要跳表
5883

5984
跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是 `O(logn)`
6085

6186
跳表的空间复杂度是 `O(n)`。不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。虽然跳表的代码实现并不简单,但是作为一种动态数据结构,比起红黑树来说,实现要简单多了。所以很多时候,我们为了代码的简单、易读,比起红黑树,我们更倾向用跳表。
6287

63-
## 3. 跳表的应用场景
88+
## 4. 跳表的应用场景
6489

6590
经典实现:Redis 的 Sorted Set、JDK 的 `ConcurrentSkipListMap``ConcurrentSkipListSet` 都是基于跳表实现。
6691

@@ -76,6 +101,6 @@ Redis 中的有序集合支持的核心操作主要有下面这几个:
76101

77102
其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
78103

79-
## 4. 参考资料
104+
## 5. 参考资料
80105

81106
- [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301)

0 commit comments

Comments
 (0)