当前位置: 首页 > Web前端 > JavaScript

从一个算法问题实现一个小文本diff工具

时间:2023-03-27 18:28:02 JavaScript

众所周知,很多社区都有内容审核机制。除了第一次发布,后续的修订也需要审核。当然,最残忍的办法就是从头再读一遍,但是小编肯定想弄死你。显然,这是相对低效的。比如你改了一个错字,可能看几遍就看不出来了,所以如果每次都能知道修改了什么,就像gitdiff那样,那就方便多了,而这个文章将简单地实现一个。寻找最长公共子序列要知道两个文本之间的区别,我们可以先找到它们的公共内容,其余的删除或添加。在算法中,这是一道经典题。有这道题1143.力口上的最长公共子序列。像推理题,可以用递归自上而下求解,也可以用for循环自下而上求解。使用for循环一般使用一个叫做dp的memo来存储信息。使用的维数组的具体数量取决于主题。这道题有两个变量(两个字符串的长度),所以我们用二维数组。我们定义dp[i][j]表示text1的子串从0-i和text2的子串从0-j的最长公共子序列的长度,然后我们需要考虑边界条件。首先,当i为0时,text1的子串为空串,所以不管j是什么,最长公共子序列的长度都是0,j为0。同样如此,所以我们可以初始化一个dp初值为0的数组:letlongestCommonSubsequence=function(text1,text2){letm=text1.lengthletn=text2.lengthletdp=newArray(m+1)dp.forEach((item,index)=>{dp[index]=newArray(n+1).fill(0)})}当i和j都不为0时,需要分几种情况看:1.当text1[i-1]===text2[j-1],表示这两个位置的字符相同,那么它们一定在最长的子序列中,而当前最长的子序列取决于前面的子串,即dp[i][j]=1+dp[i-1][j-1];2、当text1[i-1]!==text2[j-1]时,显然dp[i][j]完全取决于前面的情况,一共有三种:dp[i-1][j-1],dp[i][j-1],dp[i-1][j],但是可以排除第一种情况,因为显然没有后两种情况那么长,因为后两种情况有一个比第一种情况多了一个字符,所以长度可能多了1,那么我们取后两种情况下两种情况的最优值就够了;那么我们只需要一个双循环遍历二维数组的所有情况:letlongestCommonSubsequence=function(text1,text2){letm=text1.lengthletn=text2.length//初始化一个二维数组letdp=newArray(m+1).fill(0)dp.forEach((item,index)=>{dp[index]=newArray(n+1).fill(0)})for(leti=1;i<=m;i++){//由于i和j都是从1开始,所以我们需要减去1lett1=text1[i-1]for(letj=1;j<=n;j++){lett2=text2[j-1]//情况1if(t1===t2){dp[i][j]=1+dp[i-1][j-1]}else{//case2dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1])}}}dp[m][n]的值是最长公共子序列的长度,但是只知道长度是没有用的。我们需要知道具体的位置,我们需要再次递归。为什么我们不能在上面的循环中使用t1?===t2分支中的集合位置,因为两个字符串的所有位置都会两两比较,当有多个相同的字符时,会出现重复,如下:我们定义一个collect函数,递归判断i和j位置是否在最长子序列中,比如对于i和j位置,如果text1[i-1]===text2[j-1],那么显然这两个位置在最长子序列中,那么只要确定i-1和j-1的位置,以此类推。如果当前位置不一样,我们可以根据dp数组来判断,因为此时我们已经知道了整个dp数组的值:所以不需要每个位置都去尝试,不会造成重复,这样如dp[i-1]>dp[j],那么接下来要判断的是i-1和j的位置,否则判断i和j-1的位置的条件是i和j有一个位置已经达到0:letarr1=[]letarr2=[]letcollect=function(dp,text1,text2,i,j){if(i<=0||j<=0){return}if(text1[i-1]===text2[j-1]){//收集两个字符串中相同字符的索引arr1.push(i-1)arr2.push(j-1)returncollect(dp,text1,text2,i-1,j-1)}else{if(dp[i][j-1]>dp[i-1][j]){returncollect(dp,text1,text2,i,j-1)}else{returncollect(dp,text1,text2,i-1,j)}}}结果如下:可以看到倒序了,不喜欢也可以排序:arr1.sort((a,b)=>{返回a-b});arr2.sort((a,b)=>{返回a-b});到这里还没结束,我们还要根据最长的子序列来计算删除和添加的位置,这个比较简单,直接遍历两个字符串,不在arr1和arr2中的字符删除或添加:letgetDiffList=(text1,text2,arr1,arr2)=>{letdelList=[]letaddList=[]//遍历旧字符串for(leti=0;i{{index+1}}

然后做两两比较:exportdefault{data(){return{oldTextArr:[],newTextArr:[],showTextArr:[]}},mounted(){this.diff()},methods:{diff(){//拆分旧的并将新文本放入数组this.oldTextArr=oldText.split(/\n+/g)this.newTextArr=newText.split(/\n+/g)letlen=this.newTextArr.lengthfor(letrow=0;row{letpos=index+addTagLength;//截取当前位置之前的字符串letpre=newText.slice(0,pos);//截取以下字符串letpost=newText.slice(pos+1);newText=pre+`${newText[pos]}`+post;addTagLength+=25;//的长度为25});this.showTextArr.push(newText);}效果如下:删除有点麻烦,因为显然删除的字符在新文本中是不存在的,如果没有,我们需要找出它应该在哪里删掉,然后插回这里,我们画个图看看:先看删掉的flash,它在旧字符串中的位置是3,通过最长公共子序列,我们可以找到它前面那个字符的索引在新列表中,那么很明显index后面是新字符串中被删除字符的位置:先写一个函数获取Indexinnewtext中被删除的字符:getDelIndexInNewTextIndex(index,oldArr,newArr){for(leti=oldArr.length-1;i>=0;i--){if(index>oldArr[i]){返回newArr[i]+1;}}return0;}}下一步是计算字符串中的具体位置。对于Flash,其位置计算如下:mark(row,oldArr,newArr){//...//遍历已删除索引数组delList.forEach((index)=>{letnewIndex=this.getDelIndexInNewText指数(指数,oldArr,newArr);//之前添加的字符数letaddLength=addList.filter((item)=>{returnitem{returnitem${oldText[index]}`+post;});this.showTextArr.push(newText);}这里闪烁的位置就知道了看效果:可以看到后面乱七八糟的,原因很简单,对于Jing,我们没有把它加到占用的位置上bythenewinsertedflash://插入字符所占的位置letinsertStrLength=0;delList.forEach((index)=>{letnewIndex=this.getDelIndexInNewTextIndex(index,oldArr,newArr);letaddLength=addList.filter((item)=>{returnitem{returnitem${oldText[index]}`+post;//x的长度为26insertStrLength+=26;});到这里我们马马虎虎diff工具完成:存在的问题相信聪明的你一定会发现上面的实现有问题。如果我完全删除某行,或者完全添加新行,那么首先,新旧行的数量就会不同。现在,首先修复diff函数:diff(){this.oldTextArr=oldText.split(/\n+/g);this.newTextArr=newText.split(/\n+/g);//如果新旧行数不同,则用空字符串填充letoldTextArrLen=this.oldTextArr.length;让newTextArrLen=this.newTextArr.length;让diffRow=Math.abs(oldTextArrLen-newTextArrLen);如果(diffRow>0){让fixArr=oldTextArrLen>newTextArrLen?this.newTextArr:this.oldTextArr;for(leti=0;i{switch(item[0]){case0:htmlStr+=item[1];break;case1:htmlStr+=`${item[1]}`;break;case-1:htmlStr+=`${item[1]}`;break;default:break;}});this.showTextArr=htmlStr.split(/\n+/);}21432个字符的测量差异时间大约需要4毫秒,或者很快。嗯,以后小编可以愉快的钓鱼啦~综上所述,本文简单做了一道【求最长公共子序列】的算法题,分析了它在文本diff中的实际应用,但是我们简单的算法毕竟可以'支持实际项目,所以如果你有相关需求,可以使用文中介绍的一个开源库。完整示例代码:https://github.com/wanglin2/text_diff_demo