在大学或在准备技术访谈时,您可能听说过Levenshtein距离。这是着名的编辑距离问题。这是必须尝试的动态规划挑战之一。
你还在那儿?即使我说过“动态编程”之后?哈哈。好的!因为与你不同,我擅长逃离它;大多数时候。但我现在坐下来学习+练习这些东西。
这个惊人的书在让这个话题对我不那么吓人的作用。我最喜欢我的是作者试图解释动态规划一些实际应用的现场。
在我读到那里的所有应用中,其中两个是实用的,实际上我几乎每天使用的东西。一个是在差异工具中,如git diff来比较文本,另一个是在拼写检查器中弄错了我们键入的拼写的最接近的匹配词。
那太棒了!这些是一些“开放并阅读源代码”的东西。猜猜是什么,我喜欢这些东西。我可能会把它作为一个有趣的机会学习在野外使用的代码!
因此,让我们了解问题,在一些开源软件中实现的方式(由很多人使用 - 就像一个真正真正的大数字)。
非正式地,两个单词之间的Levenshtein距离是将一个单词更改为另一个单词所需的单个字符编辑(插入,删除或替换)的最小数量。
考虑拼写检查。当您输入某些内容时,拼写检查软件应该建议您通常是具有最小列汀距离的有效英语单词。
这是酷的东西:git使用动态编程。除了关于GIT Diff的那本书中的提及,我发现了Levenshtein距离在Git的其他一些部分的优雅用例。
这是我如何发现这一点。我通常最终每天至少键入一次git子命令。 git会智能地了解我正在尝试键入和输出这样的建议
那么,Git在这里做什么?它只是拼写检查我通过与所有有效的GIT子命令进行比较来键入的子命令。很酷,在那个时间点我就像“我认为git可能正在使用Levenshtein距离来做这个”。
然后我开始使用git源代码中的“最相似的命令”以深入挖掘源代码。
int(const char * string1,const char * string2,int swap_penalty,int superitue_penalty,int插入_penalty,int deletion_penalty);
这是标题文件,它声明函数和逻辑的C实现。
它在HELL.C中调用,负责在GIT中显示帮助消息的源文件。
该功能首先加载Git的所有有效子命令(包括我们所拥有的别名 - 只知道挖掘源的情况)。
我注意到的一个有趣的事情是代码是多么实用。如果我们只是将其视为面试准备,我们可能只会编写递归实施,并使用它(通常比写入迭代方法更简单)。但在真实世界中,递归方法可能会导致堆栈溢出。因此,解决这个问题的迭代方法是更理想的。
*该想法是为两个字符串的子字符串构建一个距离矩阵。为避免大的空间复杂性,只有最后三行*保存在内存中(如果换档与一个删除*加一个插入相同或更高的成本,则只需要两行)。
在任何特定的时间点,它们只是使用3行的备忘录来弄清答答案,而不是将整个备忘录表保持在内存中。
int(const char * string1,const char * string2,int w,int s,int a,int d){int len1 = strlen(string1),len2 = strlen(string2); int * row0,* row1,* row2; int i,j; AlloC_Array(Row0,Len2 + 1); AlloC_Array(Row1,Len2 + 1); AlloC_Array(Row2,Len2 + 1); // ........... // ........... i = row1 [len2];免费(Row0);免费(Row1);免费(Row2);返回我; }
经过一段时间盯着Git源代码今天用一点点斗争读取C并进入解决方案的所有细节,我也想在一些其他程序中阅读实施。有很多命令行工具,我注意到很多好的都支持这个拼写检查功能。
实际上,这种功能通常应该由子命令解析CLI库提供,以便它可以更容易地获得CLI作家。我又一次!自从我最近进入,我尝试通过名为Cobra的着名命令行解析器库进行搜索。
COBRA用于许多GO项目,如Kubernetes,Hugo和Github Cli,以命名几个
很酷,它甚至可以根据需要选择距离的配置。
// LD比较两个字符串并返回它们之间的Levenshtein距离。 func(s,t字符串,ignoreCase bool){如果IgnoreCase {s = strings.tolower(s)t = strings.tolower(t)} d:= make([] [] int,len(s)+ 1) i:=范围d {d [i] =给出i:= j:=范围d [0的范围d {d [i] [0] = i} [0 j:= 1的{d [0] [j] = j}; j< len(t); J ++ {为i:= 1;我< = len; I ++ {如果s [i -1] == t [j -1] {d [i] [j] = d [i -1] els {min:= d [i -1] [ j]如果d [i] [j -1]<最小{min = d [i] [j -1]}如果d [i -1] [j -1]<最小{min = d [i -1] [j -1]} d [i] [j] = min + 1}}返回d [len(t)] [len(t)]}
这是一个迭代解决方案。要更好地了解,我将函数复制到去游乐场上,并开始播放它。
现在让我试着逐行排队,并试图弄清楚发生了什么。
我们接受两个输入字符串s和t。如果设置标志,我们会忽略案例。
之后,我们创建一个名为D的二维数组,以充当备忘录表。 现在要对事物进行可视化,我正在添加一个打印备忘录表的小函数。 " 一个C" 0 0 0 A 0 0 0 B 0 0 0 C 0 0 0 接下来,我们使用行索引的值初始化每一行的第一列。 " 一个C" 0 0 0 A 1 0 0 B 2 0 0 C 3 0 0 之后,我们将每列的第一行初始化为列索引的值。 " 一个C" 0 1 2 A 1 0 0 B 2 0 0 C 3 0 0
完成基本情况:“当您从空字符串开始并开始构建字符串时,操作计数只会增加一个,因为它涉及一个插入操作。
对于J:= 1; j< len(t); J ++ {为i:= 1;我< = len; i ++ {// logic}}
啊很好,这觉得这么容易。如果行和列字母值是相同的,则我们不需要执行任何操作,从而通过查看对角线来使用最后计数的操作值。
例如:这是最终表。请注意如何(A,A)填写,查看对角线(",")。
"一个c,因为,当生成的字符串已经是`a时,不需要执行任何" 0 1 2 A 1 0 1 B 2 1 1 C 3 2 1
如果字母表不同,则我们需要执行其中一个操作(插入,删除,替换)到达所需的字母表。看起来我们弄清楚这一点是通过找到相邻单元的最小单元(已经填充的)并加入1(以有效地说我们需要执行一个操作)
最小:= D [I -1] [J]如果d [i] [j -1]<最小{min = d [i] [j -1]}如果d [i -1] [j -1]<最小{min = d [i -1] [j -1]} d [i] [j] = min + 1
特别是在上面的代码解释的最后一部分中发生的情况可能并不是明显的(因为没有试图在这里解释)。 建议是观看/阅读一些关于备忘录表的填充方式的教程。 这种解释的重点是分享填写背后的想法的清晰度如何在库代码中实现备忘录表。 我觉得这里的主要困难是备忘录表背后的想法的到来。 什么应该是列和行? 什么应该填补细胞中的值? 我们如何计算每个单元格的值? 除了采访准备的观点外,知道这些技术(只知道世界上存在的这些技术)可以是一个解决问题的求解工具箱的良好补充。 下次您正在使用CLI应用程序或其他需要拼写检查的其他应用程序,您知道该怎么办!