Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions Contents/05.Hash-Table/01.Hash-Table.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
## 1. 哈希表简介

> **哈希表(Hash Table)**:也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。也就是说,它通过键 `key` 和一个映射函数 `Hash(key)` 计算出对应的值 `value`,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。
> **哈希表(Hash Table)**:也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。
>
> 哈希表通过「键 `key` 」和「映射函数 `Hash(key)` 」计算出对应的「值 `value`」,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。

哈希表的关键思想是使用哈希函数,将键 `key` 映射到对应表的某个区块中。我们可以将算法思想分为两个部分:

- 向哈希表中插入一个关键字:哈希函数决定该关键字的对应值应该存放到表中的哪个区块,并将对应值存放到该区块中。
- 在哈希表中搜索一个关键字:使用相同的哈希函数从哈希表中查找对应的区块,并在特定的区块搜索该关键字对应的值。
- **向哈希表中插入一个关键码值**:哈希函数决定该关键字的对应值应该存放到表中的哪个区块,并将对应值存放到该区块中。
- **在哈希表中搜索一个关键码值**:使用相同的哈希函数从哈希表中查找对应的区块,并在特定的区块搜索该关键字对应的值。

哈希表的原理示例图如下所示:

![](https://qcdn.itcharge.cn/images/20220114120000.png)

在上图例子中,我们使用 `value = Hash(key) = key // 1000` 作为哈希函数。`//` 符号代表整除。我们以这个例子来说明一下哈希表的插入和查找策略。

- 插入:通过哈希函数解析关键字,并将对应值存放到该区块中。
- **向哈希表中插入一个关键码值**:通过哈希函数解析关键字,并将对应值存放到该区块中。
- 比如:`0138` 通过哈希函数 `Hash(key) = 0138 // 100 = 0`,得出应将 `0138` 分配到`0` 所在的区块中。
- 查找:通过哈希函数解析关键字,并在特定的区块搜索该关键字对应的值。
- **在哈希表中搜索一个关键码值**:通过哈希函数解析关键字,并在特定的区块搜索该关键字对应的值。
- 比如:查找 `2321`,通过哈希函数,得出 `2321` 应该在 `2` 所对应的区块中。然后我们从 `2` 对应的区块中继续搜索,并在 `2` 对应的区块中成功找到了 `2321`。
- 比如:查找 `3214`,通过哈希函数,得出 `3214` 应该在 `3` 所对应的区块中。然后我们从 `3` 对应的区块中继续搜索,但并没有找到对应值,则说明 `3214` 不在哈希表中。

Expand All @@ -29,8 +31,8 @@

- 存放所有拼音和对应地址的表可以看做是 **「哈希表」**。
- `赞` 字的拼音索引 `zan` 可以看做是哈希表中的 **「关键字 `key`」**。
- 根据拼音索引 `zan` 可以确定字对应页码的过程可以看做是哈希表中的 **「哈希函数 `Hash(key)`」**。
- 所查找到的对应页码 `599` 可以看做是哈希表中的 **「哈希地址 `value`」**。
- 根据拼音索引 `zan` 来确定字对应页码的过程可以看做是哈希表中的 **「哈希函数 `Hash(key)`」**。
- 查找到的对应页码 `599` 可以看做是哈希表中的 **「哈希地址 `value`」**。

## 2. 哈希函数

Expand All @@ -49,22 +51,21 @@

### 2.1 直接定址法

- 直接定址法:取关键字或者关键字的某个线性函数值为哈希地址。即:`Hash(key) = key` 或者 `Hash(key) = a * key + b`,其中 `a` 和 `b` 为常数。
- **直接定址法**:取关键字本身 / 关键字的某个线性函数值 作为哈希地址。即:`Hash(key) = key` 或者 `Hash(key) = a * key + b`,其中 `a` 和 `b` 为常数。

这种方法计算最简单,且不会产生冲突。适合于关键字分布基本连续的情况,如果关键字分布不连续,空位较多,则会造成存储空间的浪费。

举一个例子,假设我们有一个记录了从 `1` 岁到 `100` 岁的人口数字统计表。其中年龄为关键字,哈希函数取关键字自身,如下表所示。

| 地址 | 01 | 02 | 03 | ... | 25 | 26 | 27 | ... | 100 |
| :--: | :--: | :--: | :--: | :-: | :--: | :-: | :-: | :-: | :-: |
| 人数 | 3000 | 2000 | 5000 | ... | 1050 | ... | ... | ... | ... |
| 年龄 | 1 | 2 | 3 | ... | 25 | 26 | 27 | ... | 100 |
| 年龄 | 1 | 2 | 3 | ... | 25 | 26 | 27 | ... | 100 |
| :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
| 人数 | 3000 | 2000 | 5000 | ... | 1050 | ... | ... | ... | ... |

比如我们想要查询 `25` 岁的人有多少,则只要查询表中第 `25` 项即可。

### 2.2 除留余数法

- 除留余数法:假设哈希表的表长为 `m`,取一个不大于 `m` 但接近或等于 `m` 的质数 `p`,利用取模运算,将关键字转换为哈希地址。即:`Hash(key) = key % p`,其中 `p` 为不大于 `m` 的质数。
- **除留余数法**:假设哈希表的表长为 `m`,取一个不大于 `m` 但接近或等于 `m` 的质数 `p`,利用取模运算,将关键字转换为哈希地址。即:`Hash(key) = key % p`,其中 `p` 为不大于 `m` 的质数。

这也是一种简单且常用的哈希函数方法。其关键点在于 `p` 的选择。根据经验而言,一般 `p` 取素数或者 `m`,这样可以尽可能的减少冲突。

Expand All @@ -76,14 +77,14 @@

### 2.3 平方取中法

- 平方取中法:先通过求关键字平方值的方式扩大相近数之间的差别,然后根据表长度取关键字平方值的中间几位数为哈希地址。
- **平方取中法**:先通过求关键字平方值的方式扩大相近数之间的差别,然后根据表长度取关键字平方值的中间几位数为哈希地址。
- 比如:`Hash(key) = (key * key) // 100 % 1000`,先计算平方,去除末尾的 2 位数,再取中间 3 位数作为哈希地址。

这种方法因为关键字平方值的中间几位数和原关键字的每一位数都相关,所以产生的哈希地址也比较均匀,有利于减少冲突的发生。

### 2.4 基数转换法

- 基数转换法:将关键字看成另一种进制的数再转换成原来进制的数,然后选其中几位作为哈希地址。
- **基数转换法**:将关键字看成另一种进制的数再转换成原来进制的数,然后选其中几位作为哈希地址。
- 比如,将关键字看做是 `13` 进制的数,再将其转变为 `10` 进制的数,将其作为哈希地址。

以 `343246` 为例,哈希地址计算方式如下:
Expand Down
20 changes: 12 additions & 8 deletions Contents/06.String/01.String-Basic/01.String-Basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

- **字符串名称**:字符串定义中的 `s` 就是字符串的名称。
- **字符串的值**:$a_1a_2…a_n$ 组成的字符序列就是字符串的值,一般用双引号括起来。
- **字符变量**:字符串每一个位置上的元素都是一个字符变量。字符 $a_i$ 可以是字母、数字或者其他字符。`i` 是该字符在字符串中的位置。
- **字符串的长度**:字符串中字符的数目 `n` 称为字符串的长度。
- **空串**:零个字符构成的串也成为 **「空字符串(Null String)」**,它的长度为 `0`,可以表示为 `""`。
- **字符变量**:字符串每一个位置上的元素都是一个字符变量。字符 $a_i$ 可以是字母、数字或者其他字符。$i$ 是该字符在字符串中的位置。
- **字符串的长度**:字符串中字符的数目 $n$ 称为字符串的长度。
- **空串**:零个字符构成的串也成为 **「空字符串(Null String)」**,它的长度为 $0$,可以表示为 `""`。
- **子串**:字符串中任意个连续的字符组成的子序列称为该字符串的 **「子串(Substring)」**。并且有两种特殊子串,起始于位置为 `0`、长度为 `k` 的子串称为 **「前缀(Prefix)」**。而终止于位置 `n - 1`、长度为 `k` 的子串称为 **「后缀(Suffix)」**。
- **主串**:包含子串的字符串相应的称为 **「主串」**。

Expand All @@ -22,25 +22,29 @@ str = "Hello World"

![](https://qcdn.itcharge.cn/images/20220117141211.png)

可以看出来,字符串和数组有很多相似之处。比如使用 `名称[下标]` 的方式来访问一个字符。之所以单独讨论字符串是因为:
可以看出来,字符串和数组有很多相似之处。比如同样使用 `名称[下标]` 的方式来访问一个字符。

之所以单独讨论字符串是因为:

- 字符串中的数据元素都是字符,结构相对简单,但规模可能比较庞大。
- 经常需要把字符串作为一个整体来使用和处理。操作对象一般不是某个数据元素,而是一组数据元素(整个字符串或子串)。
- 经常需要考虑多个字符串之间的操作。比如:字符串之间的连接、比较操作。

根据字符串的特点,我们可以将字符串问题分为以下几种:

- 字符串匹配问题
- 子串相关问题
- 字符串匹配问题
- 子串相关问题
- 前缀 / 后缀相关问题;
- 回文串相关问题
- 回文串相关问题
- 子序列相关问题。

## 2. 字符串的比较

### 2.1 字符串的比较操作

两个数字之间很容易比较大小,例如 `1 < 2`。而字符串之间的比较相对来说复杂一点。字符串之间的大小取决于它们按顺序排列字符的前后顺序。比如字符串 `str1 = "abc"` 和 `str2 = "acc"`,它们的第一个字母都是 `a`,而第二个字母,由于字母 `b` 比字母 `c` 要靠前,所以 `b < c`,于是我们可以说 `"abc" < "acd" `,也可以说 `str1 < str2`。
两个数字之间很容易比较大小,例如 `1 < 2`。而字符串之间的比较相对来说复杂一点。字符串之间的大小取决于它们按顺序排列字符的前后顺序。

比如字符串 `str1 = "abc"` 和 `str2 = "acc"`,它们的第一个字母都是 `a`,而第二个字母,由于字母 `b` 比字母 `c` 要靠前,所以 `b < c`,于是我们可以说 `"abc" < "acd" `,也可以说 `str1 < str2`。

字符串之间的比较是通过组成字符串的字符之间的「字符编码」来决定的。而字符编码指的是字符在对应字符集中的序号。

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
## 1. BF 算法介绍
## 1. Brute Force 算法介绍

BF 算法的全称是 **「Brute Force 算法」**,中文意思是暴力匹配算法,也可以叫做朴素匹配算法。
> **Brute Force 算法**:简称为 BF 算法。中文意思是暴力匹配算法,也可以叫做朴素匹配算法。
>
> - **BF 算法思想**:对于给定文本串 `T` 与模式串 `p`,从文本串的第一个字符开始与模式串 `p` 的第一个字符进行比较,如果相等,则继续逐个比较后续字符,否则从文本串 `T` 的第二个字符起重新和模式串 `p` 进行比较。依次类推,直到模式串 `p` 中每个字符依次与文本串 `T` 的一个连续子串相等,则模式匹配成功。否则模式匹配失败。

> **BF 算法思想**:对于给定文本串 `T` 与模式串 `p`,从文本串的第一个字符开始与模式串 `p` 的第一个字符进行比较,如果相等,则继续逐个比较后续字符,否则从文本串 `T` 的第二个字符起重新和模式串 `p` 进行比较。依次类推,直到模式串 `p` 中每个字符依次与文本串 `T` 的一个连续子串相等,则模式匹配成功。否则模式匹配失败。
![](https://qcdn.itcharge.cn/images/20220205003716.png)

## 2. BF 算法步骤
## 2. Brute Force 算法步骤

- 对于给定的文本串 `T` 与模式串 `p`,求出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。
- 同时遍历文本串 `T` 和模式串 `p`,先将 `T[0]` 与 `p[0]` 进行比较。
- 如果相等,则继续比较 `T[1]` 和 `p[1]`。以此类推,一直到模式串 `p` 的末尾 `p[m - 1]` 为止。
- 如果不相等,则将文本串 `T` 移动到上次匹配开始位置的下一个字符位置,模式串 `p` 则回退到开始位置,再依次进行比较。
- 当遍历完文本串 `T` 或者模式串 `p` 的时候停止搜索。
1. 对于给定的文本串 `T` 与模式串 `p`,求出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。
2. 同时遍历文本串 `T` 和模式串 `p`,先将 `T[0]` 与 `p[0]` 进行比较。
1. 如果相等,则继续比较 `T[1]` 和 `p[1]`。以此类推,一直到模式串 `p` 的末尾 `p[m - 1]` 为止。
2. 如果不相等,则将文本串 `T` 移动到上次匹配开始位置的下一个字符位置,模式串 `p` 则回退到开始位置,再依次进行比较。
3. 当遍历完文本串 `T` 或者模式串 `p` 的时候停止搜索。

## 3. BF 算法代码实现
## 3. Brute Force 算法代码实现

```Python
def bruteForce(T: str, p: str) -> int:
Expand All @@ -33,15 +35,15 @@ def bruteForce(T: str, p: str) -> int:
return -1 # 匹配失败,返回 -1
```

## 4. BF 算法分析
## 4. Brute Force 算法分析

BF 算法非常简单,容易理解,但其效率很低。主要是因为在匹配过程中可能会出现回溯:当遇到一对字符不同时,模式串 `p` 直接回到开始位置,文本串也回到匹配开始位置的下一个位置,再重新开始比较。

在回溯之后,文本串和模式串中一些部分的比较是没有必要的。由于这种操作策略,导致 BF 算法的效率很低。最坏情况是每一趟比较都在模式串的最后遇到了字符不匹配的情况,每轮比较需要进行 `m` 次字符对比,总共需要进行 `n - m + 1` 轮比较,总的比较次数为 `m * (n - m + 1) `。所以 BF 算法的最坏时间复杂度为 $O(m * n)$。
在回溯之后,文本串和模式串中一些部分的比较是没有必要的。由于这种操作策略,导致 BF 算法的效率很低。最坏情况是每一趟比较都在模式串的最后遇到了字符不匹配的情况,每轮比较需要进行 `m` 次字符对比,总共需要进行 `n - m + 1` 轮比较,总的比较次数为 `m * (n - m + 1) `。所以 BF 算法的最坏时间复杂度为 $O(m \times n)$。

在最理想的情况下(第一次匹配直接匹配成功),BF 算法的最佳时间复杂度是 $O(m)$。

在一般情况下,根据等概率原则,平均搜索次数为 $\frac{(n + m)}{2}$,所以 BF 算法的平均时间复杂度为 $O(n + m)$。
在一般情况下,根据等概率原则,平均搜索次数为 $\frac{(n + m)}{2}$,所以 Brute Force 算法的平均时间复杂度为 $O(n + m)$。

## 参考资料

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
## 1. RK 算法介绍
## 1. Rabin Karp 算法介绍

RK 算法的全称叫 **「Rabin Karp 算法」**,是由它的两位发明者 Michael Oser Rabin 和 Richard Manning Karp 的名字来命名的。RK 算法是他们在 1987 年提出的、使用哈希函数以在文本中搜寻单个模式串的字符串搜索算法。
> **Rabin Karp 算法**:简称为 RK 算法。是由它的两位发明者 Michael Oser Rabin 和 Richard Manning Karp 的名字来命名的。RK 算法是他们在 1987 年提出的、使用哈希函数以在文本中搜寻单个模式串的字符串搜索算法。
>
> - **Rabin Karp 算法思想**:对于给定文本串 `T` 与模式串 `p`,通过滚动哈希算快速筛选出与模式串 `p` 不匹配的文本位置,然后在其余位置继续检查匹配项。

> **RK 算法思想**:对于给定文本串 `T` 与模式串 `p`,通过滚动哈希算快速筛选出与模式串 `p` 不匹配的文本位置,然后在其余位置继续检查匹配项。
## 2. Rabin Karp 算法步骤

## 2. RK 算法步骤
### 2.1 Rabin Karp 算法整体步骤

### 2.1 RK 算法整体步骤

- 对于给定的文本串 `T` 与模式串 `p`,求出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。
- 通过滚动哈希算法求出模式串 `p` 的哈希值 `hash_p`。
- 再通过滚动哈希算法对文本串 `T` 中 `n - m + 1` 个子串分别求哈希值 `hash_t`。
- 然后逐个与模式串的哈希值比较大小。
- 如果当前子串的哈希值 `hash_t` 与模式串的哈希值 `hash_p` 不同,则说明两者不匹配,则继续向后匹配。
- 如果当前子串的哈希值 `hash_t` 与模式串的哈希值 `hash_p` 相等,则验证当前子串和模式串的每个字符是否真的相等(避免哈希冲突)。
- 如果当前子串和模式串的每个字符相等,则说明当前子串和模式串匹配。
- 如果当前子串和模式串的每个字符不相等,则说明两者不匹配,继续向后匹配。
- 比较到末尾,如果仍未成功匹配,则说明文本串 `T` 中不包含模式串 `p`,方法返回 `-1`。
1. 对于给定的文本串 `T` 与模式串 `p`,求出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。
2. 通过滚动哈希算法求出模式串 `p` 的哈希值 `hash_p`。
3. 再通过滚动哈希算法对文本串 `T` 中 `n - m + 1` 个子串分别求哈希值 `hash_t`。
4. 然后逐个与模式串的哈希值比较大小。
1. 如果当前子串的哈希值 `hash_t` 与模式串的哈希值 `hash_p` 不同,则说明两者不匹配,则继续向后匹配。
2. 如果当前子串的哈希值 `hash_t` 与模式串的哈希值 `hash_p` 相等,则验证当前子串和模式串的每个字符是否真的相等(避免哈希冲突)。
1. 如果当前子串和模式串的每个字符相等,则说明当前子串和模式串匹配。
2. 如果当前子串和模式串的每个字符不相等,则说明两者不匹配,继续向后匹配。
5. 比较到末尾,如果仍未成功匹配,则说明文本串 `T` 中不包含模式串 `p`,方法返回 `-1`。

### 2.2 滚动哈希算法

Expand Down Expand Up @@ -56,7 +56,7 @@ $\begin{align} Hash(ate) &= (Hash(cat) - c \times 26 \times 26) * 26 + e \times

因为哈希值过大会造成溢出,所以我们在计算过程中还要对结果取模。取模的值应该尽可能大,并且应该是质数,这样才能减少哈希碰撞的概率。

## 3. RK 算法代码实现
## 3. Rabin Karp 算法代码实现

```Python
# T 为文本串,p 为模式串,d 为字符集的字符种类数,q 为质数
Expand Down
Loading