结合网上各位大佬的文章以及自己的理解,写给自己看。具体的理论我反正是看不懂😂。
A字符串转换为B字符串的最小操作次数(操作字符包括:新增一个字符,删除一个字符,修改一个字符),被称为编辑距离,LD只是其中的一种算法(俄国科学家Levenshtein提出,又叫Levenshtein Distance)。算好编辑距离就对应的可以算相似度(编辑距离+1的倒数,如:abb变为a需要删除两个b,距离是2相似度就是是)。
计算距离先建立个二维数组。
就拿大家都用的例子
A字符串为GGATCGA。
B字符串为GAATTCAGTTA。
这个我认为最简单的就是删除A字符串第一个字符串后的所有,然后加上B字符串第一个字符串后面的所有(😂,没办法我第一眼觉得最简单的方法就是这样。),但是不是最小编辑次数。实际上的应该是如下
GGA_TC_G__A
GAATTCAGTTA
黄色的表示修改,绿色的表示新增,A到B需要修改一个G为A,新增一个T,然后新增一个A,再新增两个T,编辑距离是5。相似度是=0.16。
下面是说明
一、先建立一个矩阵,二维数组长度分别为A字符串长度和B字符串长度+1
G | A | A | T | T | C | A | G | T | T | A | ||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
G | 1 | |||||||||||
G | 2 | |||||||||||
A | 3 | |||||||||||
T | 4 | |||||||||||
C | 5 | |||||||||||
G | 6 | |||||||||||
A | 7 |
表一
二、计算剩下单元格的值
根据说明LD有如下公式;Len(A)表示A字符串的长度,Rev(A)表示A字符串的反转
- LD(A,A)=0
- LD(A,'')=Len(A)
- LD(A,B)=LD(B,A)
- LD(A,B)=LD(Rev(A),Rev(B))
- 0≤LD(A,B)≤Max(Len(A),Len(B))
- LD(A+C,B+C)=LD(A,B)
- LD(A+B,A+C)=LD(B,C)
- LD(A,B)≤LD(A,C)+LD(B,C)【LD(A,B)最小值是A==B,结果为0。最大是A或者B其中一个为空字符串,结果是MAX(Len(A),Len(B)。最小值很容易就是A==B,然后假设C==B,或者C!=B即可得出。最大值假设A为空字符串,然后假设B==C和假设B与C完全不同的情况下分别带入即可。也可用大家文章中都用的记法,“三角形,两边之和大于第三边”,或者干脆死记】
- LD(A+C,B)≤LD(A,B)+LD(B,C)
A字符串由1到n个字符组成所以有A=a1a2…an组成。
B字符串由1到m个字符组成所以有B=b1b2…bm组成。
Len(A)=N,Len(B)=M,设LD(i,j)=LD(a1a2…an,b1b2…bm)。
这里的a1是,b1同理,因为第一次用这个写文章不知道怎么把下标打出来,后面才知道用MD编辑器就行了😂。
得LD(N,M)=LD(A,B),LD(0,0)=0,LD(0,j)=j,LD(i,0)=i,所以表一的第一行和第一列就可以根据LD(0,j)和LD(i,0)算出。
a~i~表示A字符串的第i个字符。
当a~i~==b~j~的情况下,有公式LD(i,j)=LD(i-1,j-1),取当前单元格矩阵左上角的值。
否则的话取LD(i,j)=Min(LD(i-1,j-1),LD(i-1,j),LD(i,j-1))+1,也就是取当前单元格左上角,上边,左边中最小的那个值加1。
另外一种计算就是当前单元格的左上角,上边,左边都加1,取最小值,如果a~i~==b~j~,左上角的值不加1改为加0。
根据上面说的规则填写矩阵中的值。
G | A | A | T | T | C | A | G | T | T | A | ||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
G | 1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
G | 2 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 6 | 7 | 8 | 9 |
A | 3 | 2 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 8 |
T | 4 | 3 | 2 | 2 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
C | 5 | 4 | 3 | 3 | 2 | 2 | 2 | 3 | 4 | 5 | 6 | 7 |
G | 6 | 5 | 4 | 4 | 3 | 3 | 3 | 3 | 3 | 4 | 5 | 6 |
A | 7 | 6 | 5 | 4 | 4 | 4 | 4 | 3 | 4 | 4 | 5 | 5 |
矩阵中最后一个值就是编辑距离,所以LD('GGATCGA','GAATTCAGTTA')=LD(7,11)=5
算矩阵值代码
function calculation_matrix(A, B)
{
var m=A.length;
var n=B.length;
var matrix = [];
var i;
var j;
for (i=0;i<=m;i++)
{
matrix.push([]);
matrix[i] = [i];
}
for (i=0;i<=n;i++)
{
matrix[0].push(i);
}
for (i=1;i<m + 1;i++)
{
for (j=1;j<n + 1;j++)
{
var c_a_c= A[i -1];
var c_b_c= B[j -1];
if (c_a_c == c_b_c)
{
matrix[i][j] = matrix[i-1][j-1];
}
else
{
matrix[i][j] = Math.min(matrix[i-1][j], matrix[i][j-1], matrix[i-1][j-1]) + 1;
}
}
}
return matrix;
}
三、对比字符串差异
定义空字符串C和D
以矩阵右下角为起点,且有如下规则。
字符a~i~==b~j~的时候,就标记当前单元格左上角,否则取左上角,上边,右边三个单元格中值最小的一个单元格标记。如果其中有相同的值,就按左上角,上边,右边的顺序取最小值。
当单元格处于第一行的时候,下一个单元格只能是左边,当单元格在第一列的时候,下个单元格只能是上边。
由以上规则计算标记如下
G | A | A | T | T | C | A | G | T | T | A | ||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
G | 1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
G | 2 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 6 | 7 | 8 | 9 |
A | 3 | 2 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 8 |
T | 4 | 3 | 2 | 2 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
C | 5 | 4 | 3 | 3 | 2 | 2 | 2 | 3 | 4 | 5 | 6 | 7 |
G | 6 | 5 | 4 | 4 | 3 | 3 | 3 | 3 | 3 | 4 | 5 | 6 |
A | 7 | 6 | 5 | 4 | 4 | 4 | 4 | 3 | 4 | 4 | 5 | 5 |
i=7,j=11,matrix表示矩阵。
A[i-1]=B[j-1],定位matrix[6][10],i--,j--后i=6,j=10;
A[5]!=B[9],取matrix[6][10],左上角,左边,上边最小值的单元格matrix[6][9],i不变,j--;
A[5]!=B[8],取matrix[6][9],左上角,左边,上边最小值的单元格matrix[6][8],i不变,j--;
A[5]==B[7],取matrix[6][8]的左上角单元格matrix[5][7],i--,j--;
...
直到定位左上角。
标记左上角的表示相同或者是修改。将a~i~添加到C,将b~j~添加到D。
标记上边的表示删除,将a~i~添加到C,将'_'添加到D。
标记左边的表示新增,将'_'添加到C,将b~j~添加到D。
对比字符串代码部分
function backtracking(matrix, A, B) {
var i=A.length;
var j=B.length;
var fix_a='';
var fix_b='';
while (i>0 && j>0)
{
var c_a_c= A[i -1];
var c_b_c= B[j -1];
if (c_a_c == c_b_c)
{
fix_a = c_a_c+fix_a;
fix_b = c_b_c+fix_b;
--i;
--j;
} else{
var w = 0;
var l_t=matrix[i-1][j-1];
var t_=matrix[i-1][j];
var l_=matrix[i][j-1];
var min_id=Math.min(l_t,t_,l_);
if (min_id==l_t)
{
w=0;
i--;
j--;
}else if (min_id==t_) {
i--;
w=1;
} else {
j--;
w=-1;
}
if (w==0) {
//修改
fix_a = "<edit>"+c_a_c+"</edit>"+fix_a;
fix_b = "<edit>"+c_b_c+"</edit>"+fix_b;
} else if (w == -1) {
//回溯到左边,说明新增
fix_b = '<ins>'+c_b_c+'</ins>'+fix_b;
} else if (w == 1) {
//回溯到上边 说明删除
fix_a = '<del>'+c_a_c+'</del>'+fix_a;
}
}
}
if(j!=0){
//说明字符串只能修改a所有字符串,然后新增对应长度的b字符串
fix_b = '<ins>'+B.substr(0,j)+'</ins>'+fix_b;
}else if(i!=0){
//说明字符串只能删除a部分字符串
fix_a = '<del>'+A.substr(0,i)+'</del>'+fix_a;
console.log([fix_a,fix_b]);
}
console.log([fix_a,fix_b]);
return [fix_a,fix_b]
}