Minimum edit distance(levenshtein distance)(最小编辑距离)初探

最小编辑距离的定义:编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

例如将kitten一字转成sitting:

sitten(k→s)

sittin(e→i)

sitting(→g)

俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。

Thewords `computer‘ and `commuter‘ are very similar, and a change of just oneletter, p->m will change the first word into the second. The word `sport‘can be changed into `sort‘ by the
deletion of the `p‘, or equivalently, `sort‘can be changed into `sport‘ by the insertion of `p‘.

Theedit distance of two strings, s1 and s2, is defined as the minimum number ofpoint mutations required to change s1 into s2, where a point mutation is oneof:

1.   change a letter,

2.   insert a letter, or

3.   delete a letter

这个问题如何解决呢?

如果不经常做算法,那么看到这个问题会没有思路,因为把一个串儿编辑成另一个串方法应该是很多的,insert,delete,substitute组合有很多种,那么如何度量最小编辑距离呢?

下面给出一种经典的算法思路:分而治之,把复杂的问题拆解成简单的子问题(并假设子问题的解已知)。这个思路最常见的一种建模方法就是数学中的数列,用前面的已知项推出未知项。在计算机中又叫递归或者递推。例如斐波拉契数列问题。

那么在此问题中,如何能得到最小编辑距离的递推公式呢?我们思考问题最好从最简单最特殊的地方出发。我们假设有两个字符串,情形有

1.两个都是空串          d(‘‘, ‘‘) = 0   -- ‘‘= empty string

2.有一个是空串          d(s, ‘‘)  = d(‘‘, s)= |s|  -- i.e. length of s(连续删除或插入)

3.两个非空串            d(s1+ch1,s2+ch2)

此时,d(s1+ch1, s2+ch2)的结果得来无非是三种情况决定,第一种假设d(s1,s2)已知,我们把两个串的最后一个字符做替换操作,则d(s1+ch1,
s2+ch2)= d(s1, s2) + if ch1=ch2 then 0 else 1;第二种可能是假设d(s1,s2+ch2)已知,把第一个串的ch1删除,则d(s1+ch1,
s2+ch2)= d(s1,s2+ch2)+1;第三中可能是假设d(s1+ch1,s2)已知,在第一个串末尾插入ch2,则d(s1+ch1,
s2+ch2)= d(s1+ch1,s2)+1,那么到底是哪一种情况得到了d(s1+ch1,s2+ch2)肯定是最小的那个决定,因此

d(s1+ch1,s2+ch2) =min[ d(s1, s2) + if ch1=ch2 then 0 else 1 ,d(s1+ch1, s2) + 1,d(s1,s2+ch2) + 1 ]

接下来我们量化定义d[i,j]是一个长度为i的串s和一个长度为j的串t的最小编辑距离。那么

d[0,0]=0

d[0,j]=j;(前者插入j个字母或后者删除j个字母)

d[i,0]=i;(前者删除i个字母或后者插入i个字母)

d[i,j]=min{d[i-1,j-1]+(s[i]==t[j]?0:1), d[i-1,j]+1, d[i,j-1]+1 }

得到递推式后,求d[i,j]就容易了。定义一个二维数组distance[][]来存储最小编辑距离,下面试java代码:

package Algorithms;

public class EditDistanceComputer {
	private int sWeight = 1;		//替换操作substitute的权值,也就是代价overhead
	private int iWeight = 1;		//插入操作insert的权值
	private int dWeight = 1;		//删除操作delete的权值
	public static void main(String[] args){
		String s = "intention";
		String t = "execution";
		EditDistanceComputer editDC = new EditDistanceComputer();
		System.out.println(editDC.getMinEditDistance(s, t));
	}

	public void setWeight(int sWeight, int iWeight, int dWeight){
		this.sWeight = sWeight;
		this.iWeight = iWeight;
		this.dWeight = dWeight;
	}

	public int getMinEditDistance(String s, String t){
		int m = s.length();
		int n = t.length();
		//申请(m+1)*(n+1)矩阵空间
		int[][] distance = new int[m+1][n+1];
		//初始化特殊值
		for(int i=0;i<m+1;i++){
			distance[i][0] = i;
		}
		for(int i=0;i<n+1;i++){
			distance[0][i] = i;
		}
		//利用递推公式遍历填充整个距离矩阵
		for(int i=1;i<=m;i++){
			for(int j=1;j<=n;j++){
				distance[i][j] = getMin(distance[i-1][j]+dWeight, distance[i][j-1]+iWeight, distance[i-1][j-1]+(s.charAt(i-1)==t.charAt(j-1)?0:sWeight));
			}
		}

		printMatrix(distance,m+1,n+1);

		return distance[m][n];
	}
	//打印矩阵
	public void printMatrix(int[][] matrix, int rownum, int colnum){
		for(int i=rownum-1;i>=0;i--){
			for(int j=0;j<colnum;j++){
				System.out.print(matrix[i][j]+"	");
			}
			System.out.println();
		}
	}

	private int getMin(int a, int b, int c){
		return (a<b)?(a<c?a:c):(b<c?b:c);
	}
}

算法的时间复杂度O(m*n),空间复杂度O(m*n)。

我们已经计算除了最小编辑距离,那么如何把s经过distance[i][j]次操作转换为t呢?看看前面的矩阵,我们得出distance[i][j]实际上有一条路径,如果记下这条路径,那么我们就能够回溯,找到对应的操作。接下来我们定义记录每一次操作的回溯矩阵backtrace[][]

package Algorithms;
enum TraceOperator {L,D,S}; //L:LEFT D:DOWN S:SLANT
public class EditAlignment {
	private int sWeight = 1;		//替换操作substitute的权值,也就是代价overhead
	private int iWeight = 1;		//插入操作insert的权值
	private int dWeight = 1;		//删除操作delete的权值
	private int m = 0;
	private int n = 0;
	int[][] distance = null;
	TraceOperator[][] backtrace = null;
	StringBuffer sb = null;
	public static void main(String[] args){
		String s = "intention";
		String t = "execution";
		EditAlignment editDC = new EditAlignment();
		System.out.println(editDC.getMinEditDistance(s, t));
		editDC.Alignment(s, t);
	}

	public void setWeight(int sWeight, int iWeight, int dWeight){
		this.sWeight = sWeight;
		this.iWeight = iWeight;
		this.dWeight = dWeight;
	}

	public void Alignment(final String s, final String t){
		sb = new StringBuffer(s);
		System.out.println("SourceString StringBuffer before Alignment: " + sb);
		if(backtrace == null || distance == null) System.exit(-1);
		int i = m;
		int j = n;
		while(backtrace[i][j] != null){
			switch(backtrace[i][j]){
				case S:
					if(s.charAt(i-1)!=t.charAt(j-1)){
						sb.replace(i-1, i, ""+t.charAt(j-1));
						System.out.println("source string: " + sb);
						System.out.println("target string: " + t);
						System.out.println("---------------------------------------");
					}
					i--;j--;
					break;
				case L:
					sb.insert(i, t.charAt(j-1));
					j--;
					System.out.println("source string: " + sb);
					System.out.println("target string: " + t);
					System.out.println("---------------------------------------");
					break;
				case D:
					sb.deleteCharAt(i-1);
					i--;
					System.out.println("source string: " + sb);
					System.out.println("target string: " + t);
					System.out.println("---------------------------------------");
					break;
				default:
					System.exit(-1);
			}
		}
		System.out.println("SourceString StringBuffer after Alignment: " + sb);
	}

	public int getMinEditDistance(final String s, final String t){
		m = s.length();                     //看成二维矩阵的话,m对应行,也就是纵坐标,n对应列,也就是横坐标
		n = t.length();
		int a,b,c;
		distance = new int[m+1][n+1];
		backtrace = new TraceOperator[m+1][n+1];
		initMatrix(m+1, n+1);
		for(int i=1;i<=m;i++){
			for(int j=1;j<=n;j++){
				a = distance[i-1][j]+dWeight;	//deletion对于s的操作,以下都是以s为源串
				b = distance[i][j-1]+iWeight;	//insertion
				c = distance[i-1][j-1]+(s.charAt(i-1)==t.charAt(j-1)?0:sWeight);//substitution
				if(a == getMin(a,b,c)){
					distance[i][j] = a;
					backtrace[i][j]=TraceOperator.D;//deletion
				}
				else if(b == getMin(a,b,c)){
					distance[i][j] = b;
					backtrace[i][j]=TraceOperator.L;//insertiodn
				}
				else if(c == getMin(a,b,c)){
					distance[i][j] = c;
					backtrace[i][j]=TraceOperator.S;//substitution
				}
			}
		}

		printMatrix(distance,m+1,n+1);
		System.out.println();
		printMatrix(backtrace,m+1,n+1);

		return distance[m][n];
	}

	public void printMatrix(int[][] matrix, int rownum, int colnum){
		for(int i=rownum-1;i>=0;i--){
			for(int j=0;j<colnum;j++){
				System.out.print(matrix[i][j]+"	");
			}
			System.out.println();
		}
	}

	public void printMatrix(TraceOperator[][] matrix, int rownum, int colnum){
		for(int i=rownum-1;i>=0;i--){
			for(int j=0;j<colnum;j++){
				System.out.print(matrix[i][j]+"	");
			}
			System.out.println();
		}
	}

	private void initMatrix(int x, int y){
		for(int i=0;i<x;i++){
			distance[i][0] = i;
		}
		for(int i=0;i<y;i++){
			distance[0][i] = i;
		}

		for(int i=1;i<x;i++){
			backtrace[i][0] = TraceOperator.D ;
		}
		for(int i=1;i<y;i++){
			backtrace[0][i] = TraceOperator.L;
		}
	}

	private int getMin(int a, int b, int c){
		return (a<b)?(a<c?a:c):(b<c?b:c);
	}
}

算法的第一次改进:

原来的算法是创建一个大小为s*t的矩阵。如果所有字符串加起来是1000个字符那么长的话,那么这个矩阵就会是1M;如果字符串是10000个字符,那么矩阵就是100M。如果元素都是整数(这里是指数字,Int32)的话,那么矩阵就会是4*100M
== 400MB这么大。

现在的算法版本只使用2*t个元素,这使得后面给出的例子成为2*10,000*4 = 80 KB。其结果是,不但内存占用更少,而且速度也变快了!因为这使得内存分配只需要很少的时间来完成。当两个字符串的长度都是1k左右时,新算法的效率是旧算法的两倍!

来看看改进的算法吧,对于计算编辑距离,如果我们不需要回溯,而是只想知道两者的相似度,那么上面的算法存储空间就是可以改进的,仔细观察你会发现递推公式d[i,j]=min{ d[i-1,j-1]+(s[i]==t[j]?0:1), d[i-1,j]+1, d[i,j-1]+1}的计算过程以及距离矩阵,你会发现当前距离的计算只和前一行以及当前行有关,即每次计算都只需要斜向的[i-1,j-1]、横向的[i,j-1]和纵向的[i-1,j]。而我们现在不需要知道中间结果,只需要最终结果,那么可以只要两行存储空间,进行迭代计算即可。现在只需要cur_row[]和pre_row[]两个向量空间即可。下面是改进的代码:

package Algorithms;

public class EditDistanceComputer1 {
	private int sWeight = 1;		//替换操作substitute的权值,也就是代价overhead
	private int iWeight = 1;		//插入操作insert的权值
	private int dWeight = 1;		//删除操作delete的权值
	public static void main(String[] args){
		String s = "GUMBO";
		String t = "GAMBOL";
		EditDistanceComputer1 editDC = new EditDistanceComputer1();
		System.out.println(editDC.getMinEditDistance(s, t));
	}

	public void setWeight(int sWeight, int iWeight, int dWeight){
		this.sWeight = sWeight;
		this.iWeight = iWeight;
		this.dWeight = dWeight;
	}

	public int getMinEditDistance(String s, String t){
		int m = s.length();
		int n = t.length();
		int[] cur_row = new int[n+1];
		int[] pre_row = new int[n+1];
		int[] temp = null;
		for(int i=0;i<n+1;i++){
			pre_row[i] = i;
		}

		for(int i=1;i<=m;i++){
			cur_row[0] = i;
			for(int j=1;j<=n;j++){
				cur_row[j] = getMin(pre_row[j]+dWeight, cur_row[j-1]+iWeight, pre_row[j-1]+(s.charAt(i-1)==t.charAt(j-1)?0:sWeight));
			}

			printVector(cur_row,n+1);
			printVector(pre_row,n+1);
			System.out.println();
			//交换当前行和先前行,为进行下一轮迭代做准备,腾出pre_row的位置
			temp = cur_row;
			cur_row = pre_row;
			pre_row = temp;
		}

		return pre_row[n];
	}

	public void printVector(int[] vector,int colnum){

			for(int j=0;j<colnum;j++){
				System.out.print(vector[j]+"	");
			}
			System.out.println();
	}

	private int getMin(int a, int b, int c){
		return (a<b)?(a<c?a:c):(b<c?b:c);
	}
}

改进后的算法时间复杂度O(m*n),空间复杂度O(2*n)

下图是对上述计算过程的解释:

最后,这个算法的时间复杂度还是O(m*n),空间复杂度O(2*n),其实还有其他算法,在某些应用场景更加高效,目前先写到这儿。当前最高效的算法是某个公司的商业机密。不过,关于最小编辑距离应用非常广泛,小到我们平时使用的IDE的代码自动补全,代码提示,搜索引擎关键词提示等等,大到远程屏幕更新,压缩传输字符串,以及机器识别中的距离度量等,都有这方面的原理。

参考:

Minimum edit distance

http://web.stanford.edu/class/cs124/lec/med.pdf

Dynamic ProgrammingAlgorithm (DPA) for Edit-Distance

http://www.allisons.org/ll/AlgDS/Dynamic/Edit/

AN EXTENSION OF UKKONEN‘SENHANCED DYNAMIC PROGRAMMING ASM
(Approximate string matching)ALGORITHM

http://www.berghel.net/publications/asm/asm.php

Fast Approximate String Matching in a Dictionary

http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.21.3317&rep=rep1&type=pdf

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-07 16:14:16

Minimum edit distance(levenshtein distance)(最小编辑距离)初探的相关文章

通俗解析莱文斯坦距离(Levenshtein Distance)计算原理(最小编辑距离)

[版权声明]:本文章由danvid发布于http://danvid.cnblogs.com/,如需转载或部分使用请注明出处 最近看到一些动态规划的东西讲到莱文斯坦距离(编辑距离)的计算,发现很多都讲的不是很清楚,比较难理解,自己思考过后重新给大家讲解一下: 维基百科解析:莱文斯坦距离,又称Levenshtein距离,是编辑距离的一种.指两个字串之间,由一个转成另一个所需的最少编辑操作次数.允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符.例如将kitten转成sittin

最小编辑距离(Minimum edit distance)

最小编辑距离是计算欧式距离的一种方法,可以被用于计算文本的相似性以及用于文本纠错,因为这个概念是俄罗斯科学家 Vladimir Levenshtein 在1965年提出来的,所以编辑距离又称为Levenshtein距离. 简单的理解就是将一个字符串转换到另一个字符串所需要的代价(cost),付出的代价越少表示两个字符串越相似,编辑距离越小,从一个字符串转换到另一个字符串简单的归纳可以有以下几种操作,1.删除(delete)2.插入(insert)3.修改(update),其中删除和插入的代价可以

C#实现Levenshtein distance最小编辑距离算法

Levenshtein distance,中文名为最小编辑距离,其目的是找出两个字符串之间需要改动多少个字符后变成一致.该算法使用了动态规划的算法策略,该问题具备最优子结构,最小编辑距离包含子最小编辑距离,有下列的公式. 其中d[i-1,j]+1代表字符串s2插入一个字母才与s1相同,d[i,j-1]+1代表字符串s1删除一个字母才与s2相同,然后当xi=yj时,不需要代价,所以和上一步d[i-1,j-1]代价相同,否则+1,接着d[i,j]是以上三者中最小的一项. 算法实现(C#): 假设两个

java文本相似度计算(Levenshtein Distance算法(中文翻译:编辑距离算法))----代码和详解

算法代码实现: package com.util; public class SimFeatureUtil { private static int min(int one, int two, int three) { int min = one; if (two < min) { min = two; } if (three < min) { min = three; } return min; } public static int ld(String str1, String str2)

字符串相似度算法(编辑距离算法 Levenshtein Distance)(转)

在搞验证码识别的时候需要比较字符代码的相似度用到“编辑距离算法”,关于原理和C#实现做个记录. 据百度百科介绍: 编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同.许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符. 例如将kitten一字转成sitting: sitten (k→s) sittin (e→i) sitting (→g) 俄罗斯科学家V

Edit Distance (or Levenshtein Distance) python solution for leetcode EPI 17.2

https://oj.leetcode.com/problems/edit-distance/ Edit Distance  Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2. (each operation is counted as 1 step.) You have the following 3 operations permitted

Levenshtein distance 编辑距离算法

这几天再看 virtrual-dom,关于两个列表的对比,讲到了 Levenshtein distance 距离,周末抽空做一下总结. Levenshtein Distance 介绍 在信息理论和计算机科学中,Levenshtein 距离是用于测量两个序列之间的差异量(即编辑距离)的度量.两个字符串之间的 Levenshtein 距离定义为将一个字符串转换为另一个字符串所需的最小编辑数,允许的编辑操作是单个字符的插入,删除或替换. 例子 ‘kitten’和’sitten’之间的 Levensht

Minimum Edit Distance with Dynamic Programming

1. Question / 实践题目 2. Analysis / 问题描述 3. Algorithm / 算法描述 3.1. Substitution 3.2. Insertion 3.3. Deletion 3.4. Sepcial Cases 3.5. Equation 4. Fill the table / 填表 4.1. Dimention 4.2. Range 4.3. Order 4.4. Related Code 5. Show Me the Code / 完整代码 6. T(n)

计算两组标签相似度算法——levenshtein distance 编辑距离算法

标签在数据分析中起到很重要的作用,给用户打标签,给商品打标签,给新闻打标签,好的标签可以为我们后期分析数据时提供很大的便利.有时我们需要计算两个对象之间标签的相似度.目前学习的算法是levenshtein distance 编辑距离算法. 代码示例: //标签相似度 public static double levenshtein(String s1, String s2) { System.out.println("levenshtein str1:"+s1+" str2: