一、levenshtein()到底是什么
先把这个函数的本质说清楚。
levenshtein()返回的是两个字符串之间的Levenshtein距离,也就是把一个字符串变成另一个字符串所需的最少单字符编辑操作次数。允许的操作有三种:
-
插入(insertion):在某个位置插入一个字符
-
替换(substitution):把某个字符换成另一个字符
-
删除(deletion):删掉某个字符
这个距离在信息论和自然语言处理里叫编辑距离(EditDistance),由苏联数学家VladimirLevenshtein于1965年提出。PHP把它内置成函数,底层用C实现,日常调用很方便。
函数签名如下:
levenshtein(
string $str1,
string $str2,
int $cost_ins = 1,
int $cost_rep = 1,
int $cost_del = 1
): int
前两个参数必填,后三个是插入、替换、删除的权重,默认都是1。返回值是整数距离。
这里有个历史遗留问题需要单独拎出来说:在PHP8.0之前,如果任一字符串超过255个字符,函数直接返回-1。这不是错误码,是一个硬性限制。PHP8.0之后这个限制被移除了,但如果你还在维护老项目,这个坑必须知道。
二、编辑距离的计算逻辑
理解算法本身,比记住函数调用更有价值。
假设要计算"HelloWorld"和"elloWorld"的距离。直观上看,前者比后者多了一个H,删掉它就能变成后者,所以距离是1。
再看"HelloWorld"和"lloWorld"。前者开头是He,后者开头是l,需要删掉H和e两个字符,距离是2。
这两个例子说明了一个关键点:levenshtein()只关心字符层面的差异,不关心语义。它不知道He和l有什么关系,只知道要删两个字符。
底层实现用的是动态规划,维护一个二维矩阵,时间复杂度是O(m×n),m和n分别是两个字符串的长度。这也是为什么长字符串会比较慢——后面会展开讲。
三、参数权重的实际意义
默认三个cost都是1,意味着插入、替换、删除被视为同等代价。但实际业务里,这三种操作的"成本"往往不一样。
举个例子。做用户输入纠错时,替换通常比删除更常见——用户打错字多半是敲错了键,而不是多打或少打。这时候可以把cost_rep调低,让替换的"惩罚"更小,距离结果更贴近真实意图。
再比如做DNA序列比对,插入和删除的生物学意义不同,权重需要按领域知识来设。
示例:把替换成本设为2,插入和删除保持1:
<?php
// 代码号学习编程示例:自定义替换权重
$dist = levenshtein("Hello PHP", "ello PHP", 10, 20, 30);
echo $dist; // 输出 30
?>
这里"HelloPHP"变成"elloPHP"只需要删除一个H,删除成本是30,所以结果是30。如果把删除成本改成1,结果就是1。权重直接决定了距离的数值,不改变算法结构。
四、大小写问题:一个容易被忽略的细节
原文提到"levenshtein()函数不区分大小写",这个说法需要纠正。
实际情况是:levenshtein()是区分大小写的。它按字节比较,H和h是两个不同的字符,会计入距离。
看这个例子:
<?php
// 代码号学习编程示例:大小写敏感验证
$dist = levenshtein('javatpoint', 'VATPOINT');
echo $dist; // 输出 10
?>
javatpoint和VATPOINT长度相同,但前5个字符大小写不一致,每个位置都需要替换,加上后面point部分也有差异,最终距离是10。
如果业务上需要忽略大小写,正确做法是先统一转成小写或大写再比较:
<?php
// 代码号学习编程示例:忽略大小写的比较
$str1 = strtolower('javatpoint');
$str2 = strtolower('VATPOINT');
echo levenshtein($str1, $str2); // 输出 0
?>
这个细节在做搜索建议、拼写纠错时特别关键。很多开发者第一次用这个函数,发现结果和预期不符,往往就是栽在这里。
五、项目中的几个坑
坑一:255字符限制
PHP8.0之前,字符串超过255字符返回-1。这个限制在对比长文本时是致命的。如果项目还跑在PHP7.x,要么截断字符串,要么自己实现编辑距离算法,要么升级PHP版本。
坑二:性能问题
O(m×n)的时间复杂度意味着,两个1000字符的字符串比较,需要计算100万个单元格。如果在一个循环里对上万条数据做模糊匹配,性能会急剧下降。
实际项目中的做法是:先用其他手段缩小候选集,再用levenshtein()精排。比如先用similar_text()或soundex()做粗筛,或者用数据库的LIKE先过滤一轮。
坑三:与similar_text()的选型
PHP还有一个similar_text()函数,返回的是相似字符数或相似百分比。两者的区别在于:
-
levenshtein()返回编辑操作次数,越小越相似 -
similar_text()返回相似度,越大越相似
如果业务需要"距离"这个语义,用levenshtein;如果需要"相似百分比",用similar_text。但similar_text()的算法复杂度更高,长字符串下比levenshtein()还慢。
六、本节课程知识要点
-
levenshtein()计算的是插入、替换、删除三种操作的最少次数 -
三个cost参数可以自定义,默认均为1
-
函数区分大小写,需要忽略大小写时先做
strtolower()处理 -
PHP8.0之前有255字符限制,超长返回-1
-
时间复杂度O(m×n),长字符串或大批量比较需要做性能优化
-
与
similar_text()的选型取决于业务需要"距离"还是"相似度"
七、一个完整的示例
假设做一个简单的命令拼写纠错功能,用户输入的命令可能有拼写错误,需要从预定义命令列表里找最接近的:
<?php
// 代码号学习编程示例:命令拼写纠错
$commands = ['install', 'update', 'remove', 'search', 'list'];
$input = 'instal';
$bestMatch = '';
$minDist = PHP_INT_MAX;
foreach ($commands as $cmd) {
$dist = levenshtein(strtolower($input), strtolower($cmd));
if ($dist < $minDist) {
$minDist = $dist;
$bestMatch = $cmd;
}
}
if ($minDist <= 2) {
echo "您是不是想输入:{$bestMatch}?";
} else {
echo "未找到匹配的命令。";
}
?>
instal和install的距离是1,小于阈值2,所以会提示用户。这个模式在CLI工具、搜索框自动纠错里很常见。
八、个人经验与建议
做了几年PHP项目,关于这个函数有几点体会:
第一,不要把它当成万能模糊匹配工具。它的本质是字符级编辑距离,对语义、词序、缩写无感。"apple"和"appel"距离是2,"apple"和"fruit"距离是5,但语义上后者可能更相关。如果需要语义匹配,应该用向量检索或全文索引。
第二,阈值比距离本身更重要。实际业务里很少直接用距离数值做判断,通常是设一个阈值(比如≤2认为匹配)。阈值定多少,取决于字符串平均长度和业务容忍度,需要拿真实数据跑一遍再定。
第三,长文本场景优先考虑其他方案。如果是文章级别的相似度比较,levenshtein()的性能瓶颈很明显。可以考虑SimHash、MinHash或者直接用搜索引擎的模糊匹配能力。
第四,PHP8.0升级是值得的。255字符限制解除后,这个函数的适用范围大了很多,不再需要为了长字符串写额外的截断逻辑。
九、补充:与编辑距离相关的专业术语
-
EditDistance:编辑距离,衡量两个字符串差异的通用概念
-
DynamicProgramming:动态规划,levenshtein底层使用的算法思想
-
CostMatrix:代价矩阵,动态规划中存储中间结果的二维数组
-
FuzzyMatching:模糊匹配,编辑距离的典型应用场景
-
SpellChecking:拼写检查,基于编辑距离的常见功能
这些术语在阅读英文文档或技术论文时会经常遇到,理解它们有助于更准确地把握这个函数的定位。