Skip to content

Commit 73ce833

Browse files
committed
修复和优化语句表述
1 parent 423bcbc commit 73ce833

17 files changed

+1160
-1063
lines changed

docs/04_string/04_01_string_basic.md

Lines changed: 214 additions & 95 deletions
Large diffs are not rendered by default.

docs/04_string/04_03_string_brute_force.md renamed to docs/04_string/04_02_string_brute_force.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
## 1. Brute Force 算法介绍
22

3-
> **Brute Force 算法**:简称为 BF 算法。中文意思是暴力匹配算法,也可以叫做朴素匹配算法
4-
>
5-
> - **BF 算法思想**对于给定文本串 $T$ 与模式串 $p$,从文本串的第一个字符开始与模式串 $p$ 的第一个字符进行比较,如果相等,则继续逐个比较后续字符,否则从文本串 $T$ 的第二个字符起重新和模式串 $p$ 进行比较。依次类推,直到模式串 $p$ 中每个字符依次与文本串 $T$ 的一个连续子串相等,则模式匹配成功。否则模式匹配失败
3+
> **Brute Force 算法**:简称为 BF 算法,也可以叫做「朴素匹配算法」
4+
>
5+
> - **Brute Force 算法核心思想**将模式串 $p$ 依次与文本串 $T$ 的每个起点对齐,从左到右逐字符比对;相等则继续,不等则把对齐起点右移一位,直到匹配成功或遍历完文本
66
77
![朴素匹配算法](https://qcdn.itcharge.cn/images/20240511154456.png)
88

99
## 2. Brute Force 算法步骤
1010

11-
1. 对于给定的文本串 $T$ 与模式串 $p$,求出文本串 $T$ 的长度为 $n$,模式串 $p$ 的长度为 $m$。
12-
2. 同时遍历文本串 $T$ 和模式串 $p$,先将 $T[0]$ 与 $p[0]$ 进行比较。
13-
1. 如果相等,则继续比较 $T[1]$ 和 $p[1]$。以此类推,一直到模式串 $p$ 的末尾 $p[m - 1]$ 为止。
14-
2. 如果不相等,则将文本串 $T$ 移动到上次匹配开始位置的下一个字符位置,模式串 $p$ 则回退到开始位置,再依次进行比较。
15-
3. 当遍历完文本串 $T$ 或者模式串 $p$ 的时候停止搜索。
11+
1. 设文本串 $T$ 长度为 $n$,模式串 $p$ 长度为 $m$。
12+
2. 从 $T$ 的每个起点 $0..n - m$ 依次与 $p$ 对齐,逐字符比较:如果相等则继续,不相等则起点右移一位、$p$ 归零。
13+
3. 如果某次对齐能把 $p$ 的全部字符匹配完,则返回该起点;否则无解。
1614

1715
## 3. Brute Force 算法代码实现
1816

@@ -37,13 +35,25 @@ def bruteForce(T: str, p: str) -> int:
3735

3836
## 4. Brute Force 算法分析
3937

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

42-
在回溯之后,文本串和模式串中一些部分的比较是没有必要的。由于这种操作策略,导致 BF 算法的效率很低。最坏情况是每一趟比较都在模式串的最后遇到了字符不匹配的情况,每轮比较需要进行 $m$ 次字符对比,总共需要进行 $n - m + 1$ 轮比较,总的比较次数为 $m \times (n - m + 1) $。所以 BF 算法的最坏时间复杂度为 $O(m \times n)$。
40+
| 指标 | 复杂度 | 说明 |
41+
| ------------ | -------------- | ------------------------------------ |
42+
| 最好时间复杂度 | $O(m)$ | 首个起点即匹配成功 |
43+
| 最坏时间复杂度 | $O(n \times m)$ | 每次都需回退,全部比较 |
44+
| 平均时间复杂度 | $O(n \times m)$ | 一般情况下的复杂度 |
45+
| 空间复杂度 | $O(1)$ | 原地匹配,无需额外空间 |
4346

44-
在最理想的情况下(第一次匹配直接匹配成功),BF 算法的最佳时间复杂度是 $O(m)$。
47+
- 大量回溯导致重复比较,是 BF 变慢的根源。
48+
- 当文本或模式较长时,更应考虑 KMP、BM、Sunday 等改进算法。
49+
50+
## 5. 总结
51+
52+
Brute Force(BF)算法通过将模式串与文本串每个可能的起点逐字符对齐比较,遇到不匹配时起点右移、模式串重头开始。该算法实现简单,空间复杂度为 $O(1)$,但时间复杂度较高:最好情况下为 $O(m)$(首位即匹配),平均和最坏情况下为 $O(n\times m)$,适合小规模或一次性匹配场景。
53+
54+
**优点**:实现简单、无需预处理、适合小规模或一次性匹配。
55+
**缺点**:回溯多、效率低,不适合长文本/长模式或多次匹配场景。
4556

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

4858
## 练习题目
4959

docs/04_string/04_02_string_single_pattern_matching.md

Lines changed: 0 additions & 143 deletions
This file was deleted.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
## 1. Rabin Karp 算法介绍
2+
3+
> **Rabin Karp(RK)算法**:由 Michael Oser Rabin 与 Richard Manning Karp 于 1987 年提出,是一种利用哈希快速筛查匹配起点的单模式串匹配算法。
4+
>
5+
> - **Rabin Karp 算法核心思想**:给定文本串 $T$ 与模式串 $p$,先计算 $p$ 的哈希值,再对 $T$ 的所有长度为 $m=|p|$ 的子串高效计算哈希。借助「滚动哈希」在 $O(1)$ 时间更新相邻子串的哈希,用哈希相等作为快速筛选,仅在相等时再逐字符比对以排除哈希冲突。
6+
7+
## 2. Rabin Karp 算法步骤
8+
9+
### 2.1 Rabin Karp 算法整体流程
10+
11+
1. 设 $n=|T|$、$m=|p|$。
12+
2. 计算模式串哈希 $H(p)$。
13+
3. 计算文本首个长度为 $m$ 的子串 $T_{[0,m-1]}$ 的哈希 $H(T_{[0,m-1]})$,并用滚动哈希依次得到其余 $n - m$ 个相邻子串的哈希。
14+
4. 逐一比较 $H(T_{[i,i+m-1]})$ 与 $H(p)$:
15+
- 如果不相等,跳过;
16+
- 如果相等,逐字符核验:完全相同则返回起点 $i$,否则继续。
17+
5. 全部位置检查后仍未匹配,返回 $-1$。
18+
19+
### 2.2 滚动哈希算法
20+
21+
实现 RK 的关键是 **滚动哈希**:使相邻子串哈希的更新从 $O(m)$ 降为 $O(1)$,显著提升效率。
22+
23+
滚动哈希采用 **Rabin fingerprint** 思想:把子串视作 $d$ 进制多项式,基于上一个子串的哈希在 $O(1)$ 时间得到下一个子串的哈希。
24+
25+
下面我们用一个例子来解释一下这种算法思想。
26+
27+
设字符集大小为 $d$,用 $d$ 进制多项式哈希表示子串。
28+
29+
举个例子,假如字符串只包含 $a \sim z$ 这 $26$ 个小写字母,那么我们就可以用 $26$ 进制数来表示一个字符串,$a$ 表示为 $0$,$b$ 表示为 $1$,以此类推,$z$ 就用 $25$ 表示。
30+
31+
例如 `"cat"` 的哈希可表示为:
32+
33+
$$\begin{aligned} Hash(cat) &= c \times 26^2 + a \times 26^1 + t \times 26^0 \cr &= 2 \times 26^2 + 0 \times 26^1 + 19 \times 26^0 \cr &= 1371 \end{aligned}$$
34+
35+
这种多项式哈希的特点是:相邻子串的哈希可由上一个快速推得。
36+
37+
如果 $cat$ 的相邻子串为 `"ate"`,直接计算其哈希:
38+
39+
$$\begin{aligned} Hash(ate) &= a \times 26^2 + t \times 26^1 + e \times 26^0 \cr &= 0 \times 26^2 + 19 \times 26^1 + 4 \times 26^0 \cr &= 498 \end{aligned}$$
40+
41+
如果利用上一个子串 `"cat"` 的哈希滚动更新:
42+
43+
$$\begin{aligned} Hash(ate) &= (Hash(cat) - c \times 26^2) \times 26 + e \times 26^0 \cr &= (1371 - 2 \times 26^2) \times 26 + 4 \times 26^0 \cr &= 498 \end{aligned}$$
44+
45+
可以看出,这两种方式计算出的哈希值是相同的。但是第二种计算方式不需要再遍历子串,只需要进行一位字符的计算即可得出整个子串的哈希值。这样每次计算子串哈希值的时间复杂度就降到了 $O(1)$。然后我们就可以通过滚动哈希算法快速计算出子串的哈希值了。
46+
47+
将上述规律形式化如下。
48+
49+
给定文本串 $T$ 与模式串 $p$,设 $n=|T|$、$m=|p|$、字符集大小为 $d$,则:
50+
51+
- 模式串:$H(p)=\sum\limits_{k=0}^{m-1} p_k\, d^{m-1-k}$;
52+
- 文本首子串:$H(T_{[0,m-1]})=\sum\limits_{k=0}^{m-1} T_k\, d^{m-1-k}$;
53+
- 滚动关系:$H(T_{[i+1,i+m]})=\big(H(T_{[i,i+m-1]})-T_i\, d^{m-1}\big)\, d+T_{i+m}$。
54+
55+
为避免溢出与降低冲突,计算时通常对大质数 $q$ 取模(模数宜大且为质数)。
56+
57+
## 3. Rabin–Karp 代码实现
58+
59+
```python
60+
# T: 文本串,p: 模式串,d: 字符集大小(基数),q: 模数(质数)
61+
def rabinKarp(T: str, p: str, d: int, q: int) -> int:
62+
n, m = len(T), len(p)
63+
if m == 0:
64+
return 0
65+
if n < m:
66+
return -1
67+
68+
hash_p, hash_t = 0, 0
69+
70+
# 计算 H(p) 与首个子串的哈希
71+
for i in range(m):
72+
hash_p = (hash_p * d + ord(p[i])) % q
73+
hash_t = (hash_t * d + ord(T[i])) % q
74+
75+
# 使用 pow 的三参形式避免中间溢出
76+
power = pow(d, m - 1, q) # d^(m-1) % q,用于移除最高位字符
77+
78+
for i in range(n - m + 1):
79+
if hash_p == hash_t:
80+
# 避免冲突:逐字符核验
81+
match = True
82+
for j in range(m):
83+
if T[i + j] != p[j]:
84+
match = False
85+
break
86+
if match:
87+
return i
88+
if i < n - m:
89+
# 滚动更新到下一个子串
90+
hash_t = (hash_t - power * ord(T[i])) % q # 去掉最高位字符
91+
hash_t = (hash_t * d + ord(T[i + m])) % q # 加入新字符
92+
93+
return -1
94+
```
95+
96+
## 4. 复杂度与性质
97+
98+
| 指标 | 复杂度 | 说明 |
99+
| ------------ | -------------- | ------------------------------------ |
100+
| 最好时间复杂度 | $O(n-m+1)$ | 无哈希冲突时,仅需 $n-m+1$ 次哈希对比,均为 $O(1)$,无需逐字符校验 |
101+
| 最坏时间复杂度 | $O(m(n-m+1))\approx O(nm)$ | 每次哈希均冲突,需 $n-m+1$ 次逐字符全量比对,每次 $O(m)$ |
102+
| 平均时间复杂度 | $O(n-m+1)$ | 期望哈希冲突极少,绝大多数位置仅哈希对比,均摊 $O(1)$ |
103+
| 空间复杂度 | $O(1)$ | 仅需常数变量存储哈希值与辅助参数 |
104+
105+
说明:与 BF 相比,RK 通过哈希筛选把大多数不匹配位置在 $O(1)$ 内排除;但哈希冲突会触发逐字符校验,致使最坏复杂度退化。
106+
107+
## 5. 总结
108+
109+
Rabin-Karp(RK)算法通过将模式串和文本子串转化为哈希值,利用「滚动哈希」快速筛查匹配位置,大幅减少无效字符比较。其平均时间复杂度远优于朴素算法,适合大文本和多模式串场景,但哈希冲突时需回退逐字符比对,最坏情况下复杂度与朴素法相同。合理选择哈希参数可有效降低冲突概率,是一种高效且易于扩展的字符串匹配算法。
110+
111+
**优点**
112+
- 滚动哈希使子串哈希更新为 $O(1)$,平均性能优于 BF;
113+
- 易于扩展到多模式串场景(统一维护多哈希)。
114+
**缺点**
115+
- 存在哈希冲突,最坏复杂度可退化至 $O(nm)$;
116+
- 需合理选择基数 $d$ 与大质数模 $q$,以降低冲突概率。
117+
118+
## 练习题目
119+
120+
- [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md)
121+
- [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md)
122+
- [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md)
123+
- [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md)
124+
- [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md)
125+
- [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md)
126+
127+
- [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE)
128+
129+
## 参考资料
130+
131+
- 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著
132+
- 【文章】[字符串匹配基础(上)- 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/71187)
133+
- 【文章】[字符串匹配算法 - Rabin Karp 算法 - coolcao 的小站](https://coolcao.com/2020/08/20/rabin-karp/)
134+
- 【问答】[string - Python: Rabin-Karp algorithm hashing - Stack Overflow](https://stackoverflow.com/questions/22216948/python-rabin-karp-algorithm-hashing)

0 commit comments

Comments
 (0)