levenshtein距离dp状态定义为dpi,表示将s1前i个字符变为s2前j个字符的最少编辑步数;其维度为(m+1)×(n+1),初始化dp0=j、dpi=i,转移时需对齐字符索引(i-1, j-1),滚动数组可压至o(min(m,n)),utf-8需转码点处理,传参须用const std::string&避免深拷贝。

Levenshtein距离的DP状态定义为什么是dp[i][j]?
因为dp[i][j]表示将s1前i个字符变成s2前j个字符所需的最少编辑步数,这是唯一能覆盖插入、删除、替换三种操作的最小粒度状态。如果定义成dp[i]或按字符哈希索引,就无法区分“删s1[i]”和“插s2[j]”的代价差异。
常见错误是把i和j当成字符串下标(从0开始),结果初始化错位——实际dp[0][j]对应空串变s2.substr(0,j),必须是j次插入,所以初值为j;同理dp[i][0] = i。
-
dp数组维度应为(s1.length() + 1) × (s2.length() + 1),别漏+1 - 循环从
i=1,j=1开始,避免越界访问s1[i-1]和s2[j-1] - 当
s1[i-1] == s2[j-1]时,dp[i][j] = dp[i-1][j-1],不是dp[i][j-1]或其它
如何用滚动数组把空间从O(mn)压到O(min(m,n))?
观察DP转移只依赖上一行和左上角:dp[i][j]仅由dp[i-1][j](删)、dp[i][j-1](插)、dp[i-1][j-1](换/跳过)推出。因此只需保存两行:当前行curr和上一行prev。
更进一步,若固定让短字符串做列方向(即令n = min(s1.size(), s2.size())),就能确保空间上限为O(n)。注意此时需交换输入顺序并记录是否交换,否则语义错乱。
- 先判断长度,必要时交换
s1和s2,保证s2更短 - 分配
vector<int> prev(n+1)</int>和curr(n+1),初始化prev[j] = j - 内层循环
j从1到n,每次迭代后执行swap(prev, curr),别忘了清空curr或重置首元素
为什么std::string传参要用const std::string&?
Levenshtein常用于高频比对(如拼写纠错、日志聚类),若传值会触发深拷贝,对长字符串(>1KB)开销明显。而引用传递零成本,且const保证函数内不修改原串——这在多线程或只读场景中是安全前提。
另一个容易被忽略的点:如果后续要扩展支持std::wstring或自定义字符类型,接口应模板化,但基础版先守住const std::string&这个底线。
- 不要写
int levenshtein(string s1, string s2),复制开销白送 - 若调用方传的是临时对象(如
levenshtein(getA() + getB(), "test")),const &仍可绑定,无额外成本 - VS2019+ GCC11 虽支持RVO/NRVO,但不保证所有路径生效,不依赖编译器优化
边界case:空串、中文UTF-8字符串、超长串怎么处理?
空串没问题——滚动数组初始化逻辑天然覆盖dp[0][j]和dp[i][0];但UTF-8中文会出错:一个汉字占3字节,std::string::size()返回字节数而非字符数,直接套用会导致s1[i-1]取到UTF-8中间字节,比较永远为假,距离算成len1+len2。
超长串(如50KB)则面临栈溢出风险:局部vector默认在栈分配(小尺寸优化),应改用std::vector::reserve()配合堆内存,或直接用new int[n+1]手动管理(记得delete[])。
- 处理中文必须先转Unicode码点(用
std::codecvt_utf8或第三方库如utf8cpp),再对码点数组跑DP - 生产环境建议加长度检查:
if (s1.size() > 10000 || s2.size() > 10000) return -1;防OOM - 若只需相似度百分比(0~100),最后除以
max(s1.size(), s2.size()),但注意分母为0时特判
curr[0]重置——它代表“当前行首列”,即空串变s2前j字符,每轮都要设为i(当前行号),不是继承上一轮值。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











