1.基本原理
其核心思想是:将一个字符串转换为另一个字符串所需的最少单字符编辑操作次数。
**编辑距离(Edit Distance)**衡量的是:
把字符串 A 转换成字符串 B 所需的最小编辑操作代价
最经典的是 Levenshtein Distance,允许三种操作:
| 操作 | 含义 | 代价 |
|---|---|---|
| Insert | 插入字符 | 1 |
| Delete | 删除字符 | 1 |
| Substitute | 替换字符 | 1(相同为 0) |
核心逻辑
- 编辑距离越大,字符串差异越显著;距离为 0 时,字符串完全一致。
- 与汉明距离的区别:汉明距离仅适用于等长字符串且仅支持替换操作,编辑距离无长度限制、支持三种操作,适用范围更广。
- 底层实现依赖动态规划,通过构建 DP 表避免暴力枚举的高复杂度(时间复杂度 O ( m × n ) , m / n O(m×n),m/n O(m×n),m/n 为两字符串长度)。
2.算法步骤
2.1 算法步骤讲解
🧠 动态规划的核心思想:"从小问题推大问题"
想象你有一个表格(像 Excel 表格),横着写 B 的每个字母,竖着写 A 的每个字母。
表格里的每一个格子 dp[i][j] 的意思是:
"把 A 的前 i 个字母 → 变成 B 的前 j 个字母,最少要几步?"
我们的任务,就是把这个表格一步步填满,最后右下角那个格子就是答案!
🔑 状态转移方程的通俗解释
现在,我们站在某个格子 dp[i][j] 上,想知道它的值是多少。
这时候,我们看 A 的第 i 个字母 和 B 的第 j 个字母(注意:编程里从 0 开始,所以实际是 A[i-1] 和 B[j-1])。
情况 1️⃣:两个字母一样 (比如都是 't')
- 那太好了!不需要任何操作。
- 所以当前的最小步数 = 左上角格子的值(因为前面那部分已经搞定了)。
公式 :
dp[i][j] = dp[i-1][j-1]
情况 2️⃣:两个字母不一样 (比如 'k' vs 's')
- 这时候我们必须做一次操作 ,但有三种选择,我们要选最省事的那条路:
| 操作 | 对应哪个格子? | 含义 |
|---|---|---|
| 删除 A 的当前字母 | dp[i-1][j] 上方 |
先把 A 的前 i-1 个变成 B 的前 j 个,再把 A 多出的这个字母删掉 |
| 插入 B 的当前字母 | dp[i][j-1] 左方 |
先把 A 的前 i 个变成 B 的前 j-1 个,再在末尾加上 B 的这个字母 |
| 把 A 的当前字母改成 B 的 | dp[i-1][j-1] 左上 |
先把前面都对齐了,再改这一个字母 |
👉 这三种方式都要 +1 步 (因为做了一次操作),我们取其中最小的那个。
公式 :
dp[i][j] = 1 + min( 上方, 左方, 左上方 )
🖼️ 举个小例子:A="ab", B="abc"
我们填表:
| 这一列表示A要变空白,需要删除的操作数 | |||||
|---|---|---|---|---|---|
| "" | a | b | c | ||
| 只一行表示A要从空变成B,需要插入的操作数 | "" | 0 | 1 | 2 | 3 |
| a | 1 | 0 | 1 | 2 | |
| b | 2 | 1 | 0 | 1 ← 答案! |
看右下角 dp[2][3](即 "ab" → "abc"):
- A 最后是
'b',B 最后是'c'→ 不一样! - 看三个邻居:
- 上方
dp[1][3] = 2(删'b',再变"a"→"abc"?不划算) - 左方
dp[2][2] = 0("ab"→"ab"已完成,再插'c'→ 总共 1 步 ✅) - 左上
dp[1][2] = 1("a"→"ab"花 1 步,再改'b'→'c'→ 共 2 步)
- 上方
→ 最小的是 左方 + 1 = 0 + 1 = 1 ,所以答案是 1(只需插入 'c')。
✅ 总结一句话:
当前格子的值,取决于"左边、上边、左上角"哪个路径最短,再根据当前两个字母是否相同,决定要不要多花一步。
这就像是在玩一个"文字变形迷宫",每一步都选择代价最小的走法,最终走到终点时,总步数就是编辑距离。
2.2 算法实现伪代码
设字符串 A A A 长度为 m m m, B B B 长度为 n n n。
构建一个 ( m + 1 ) × ( n + 1 ) (m+1)×(n+1) (m+1)×(n+1) 的二维数组 d p dp dp,其中 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 A [ 0 : i ] A[0:i] A[0:i] 与 B [ 0 : j ] B[0:j] B[0:j] 的编辑距离。
-
初始化
d p [ 0 ] [ j ] = j dp[0][j] = j dp[0][j]=j(空串变 B [ 0 : j ] B[0:j] B[0:j] 需 j j j 次插入)
d p [ i ] [ 0 ] = i dp[i][0] = i dp[i][0]=i( A [ 0 : i ] A[0:i] A[0:i] 变空串需 i i i 次删除) -
状态转移方程
对每个 i ∈ [ 1 , m ] , j ∈ [ 1 , n ] i∈[1,m], j∈[1,n] i∈[1,m],j∈[1,n]:if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] # 字符相同,无需操作
else:
dp[i][j] = 1 + min(
dp[i-1][j], # 删除 A[i-1]
dp[i][j-1], # 插入 B[j-1]
dp[i-1][j-1] # 替换 A[i-1] → B[j-1]
) -
结果
d p [ m ] [ n ] dp[m][n] dp[m][n] 即为最终编辑距离。
3.优缺点适用场景
| 特点 | 说明 |
|---|---|
| ✅ 核心优点 | 直观易懂 :编辑操作(增、删、改)符合人类直觉。 应用广泛 :是许多文本处理任务的基石算法。 可灵活扩展:可通过为不同操作设置不同代价来适应特定场景。 |
| ❌ 主要缺点 | 计算成本高 :基础动态规划时间复杂度为 O(m*n) ,长文本计算慢。 语义盲区 :只比较字符,不理解单词意义(如无法知道"电脑"和"计算机"是近义词)。 长度敏感:长字符串间的微小差异也会导致距离值较大。 |
| 🛠️ 典型使用场景 | 1. 拼写检查与纠错 :为输入词在词典中快速找到最相似的候选词。 2. 数据清洗 :模糊匹配数据库中的名称(如"Jon Smith"与"John Smith")。 3. 语音识别评估 :计算词错误率(WER),即识别结果与正确文本的编辑距离。 4. 生物信息学 :比较DNA/RNA序列的相似性。 5. NER中 :实体规范化、拼写纠错、边界微调。一般用于 后处理阶段 6. 实体对齐 :不单独使用结合语义网络使用 7. 本体匹配 : 概念名 / 属性名初筛,Tree / Graph Edit Distance(进阶) 8. 搜索 / OCR / 输入纠错:模糊搜索、自动补全、OCR 错误修正 |
4.主流库推荐
| 特性 | RapidFuzz |
python-Levenshtein |
textdistance |
difflib |
|---|---|---|---|---|
| 速度 | ⚡⚡⚡ 最快 | ⚡⚡ 快 | ⚡ 中等 | 🐢 慢 |
| 编辑距离支持 | ✅(需导入子模块) | ✅(distance) |
✅ | ❌(只有 ratio) |
| 高级模糊匹配 | ✅(partial/token/WRatio) | ❌ | 部分 | 有限 |
| 多线程/批量 | ✅(process.extract) |
❌ | ❌ | ❌ |
| 跨平台安装 | ✅(预编译 wheel) | ⚠️(偶有编译问题) | ✅ | ✅(标准库) |
| 维护状态 | ✅ 活跃 | ⚠️ 更新缓慢 | ✅ 活跃 | ✅(但功能固定) |
💡 结论 :RapidFuzz 是当前 Python 生态中最推荐的模糊匹配库,尤其适合工业级应用。
5.RapidFuzz 使用
1. 极致性能(C++ + SIMD 加速)
- 底层用 C++ 实现 ,并利用 SIMD 指令集(如 SSE2、AVX2)进行向量化计算;
- 在大多数 Levenshtein 距离和相似度计算任务中,比
python-Levenshtein快 2~10 倍; - 支持多线程(通过
processes参数),适合批量处理。
📊 实测参考(10k 字符串对):
RapidFuzz: ~0.8 秒python-Levenshtein: ~2.5 秒- 纯 Python DP: >30 秒
2. 统一且现代化的 API
-
所有函数命名清晰,参数一致,支持:
- 单对字符串比较
- 一对多(
fuzz.ratio,fuzz.partial_ratio) - 多对多(
process.extract,process.cdist)
-
示例:
pythonfrom rapidfuzz import fuzz, process # 基础相似度(归一化 Levenshtein) score = fuzz.ratio("kitten", "sitting") # → 57.14... # 从候选列表中找最匹配项 choices = ["apple", "appel", "appl", "banana"] best = process.extractOne("apple", choices) # → ('appel', 88.88..., 1)
3. 丰富的相似度算法
不仅支持 Levenshtein,还内置多种实用变体:
| 函数 | 说明 |
|---|---|
fuzz.ratio |
标准归一化编辑距离相似度 |
fuzz.partial_ratio |
子串匹配(适合长短不一的文本) |
fuzz.token_sort_ratio |
忽略词序(先排序再比) |
fzz.token_set_ratio |
忽略重复词和顺序(取交集+差集) |
fuzz.WRatio |
自动选择最优策略(加权混合) |
这些对实体对齐、地址/姓名模糊匹配极其有用。
4. 零依赖 & 跨平台友好
- 纯 C++ 扩展,无需系统级依赖 (不像
python-Levenshtein有时需gcc编译); - 提供预编译 wheel,完美支持 Windows、macOS(含 Apple Silicon)、Linux;
- 兼容 Python 3.7+。
5. 活跃维护 & 开源友好
- GitHub 项目活跃([github.com/maxbachmann/RapidFuzz](https://github Assistant));
- 文档完善(https://rapidfuzz.github.io/RapidFuzz/);
- MIT 许可证,可商用。
⚠️ 二、潜在缺点或限制
| 问题 | 说明 |
|---|---|
| 不直接返回编辑距离整数 | 默认返回归一化相似度 (0~100)。若需原始编辑距离,可用: from rapidfuzz.distance.Levenshtein import distance |
| 内存占用略高(批量时) | 多线程/向量化会暂用更多内存,但通常可接受 |
| 生态整合稍弱 | 不如 scikit-learn 或 pandas 原生集成(但可通过 apply 轻松使用) |
🔧 获取原始编辑距离示例:
pythonfrom rapidfuzz.distance.Levenshtein import distance d = distance("kitten", "sitting") # → 3
📦 安装与快速开始
bash
pip install rapidfuzz
python
from rapidfuzz import fuzz, process
from rapidfuzz.distance.Levenshtein import distance
# 1. 相似度(0-100)
print(fuzz.ratio("hello", "hallo")) # 80.0
# 2. 原始编辑距离
print(distance("hello", "hallo")) # 1
# 3. 从列表中找最佳匹配
choices = ["Atlanta Falcons", "New York Jets", "Dallas Cowboys"]
result = process.extractOne("atlanta falcons", choices, scorer=fuzz.WRatio)
print(result) # ('Atlanta Falcons', 100.0, 0)