leetcode 72. Edit Distance

一 题目

   Given two words word1 and word2, find the minimum number of operations required to convert word1 to word2.

You have the following 3 operations permitted on a word:

  1. Insert a character
  2. Delete a character
  3. Replace a character

Example 1:

Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation: 
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')

Example 2:

Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation: 
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')

Accepted 200,858  Submissions  505,528

二 分析

hard 级别,求两个字符串的编辑距离。

这个题目很有实际意义,文本的相似度这里,作为一种重要方式。为了方便理解,我把wiki的内容贴一下。

编辑距离是针对二个字符串(例如英文字)的差异程度的量化量测,量测方式是看至少需要多少次的处理才能将一个字符串变成另一个字符串。编辑距离可以用在自然语言处理中,例如拼写检查可以根据一个拼错的字和其他正确的字的编辑距离,判断哪一个(或哪几个)是比较可能的字。DNA也可以视为用A、C、G和T组成的字符串,因此编辑距离也用在生物信息学中,判断二个DNA的类似程度。Unix 下的 diff 及 patch 即是利用编辑距离来进行文本编辑对比的例子。

编辑距离有几种不同的定义,差异在可以对字符串进行的处理。

  • 莱文斯坦距离中,可以删除、加入、取代字符串中的任何一个字元,也是较常用的编辑距离定义,常常提到编辑距离时,指的就是莱文斯坦距离[1]
  • 也存在其他编辑距离的定义方式,例如 Damerau-Levenshtein 距离是一种莱文斯坦距离的变种,但允许以单一操作交换相邻的两个字符(称为字符转置),如 AB→BA 的距离是 1(交换)而非 2(先删除再插入、或者两次替换)。
  • LCS(最长公共子序列)距离只允许删除、加入字元[1]:37。
  • Jaro 距离只允许字符转置。
  • 汉明距离只允许取代字元[1]

例子[编辑]

kitten和sitting的莱文斯坦距离是3。将kitten变为sitting的最小处理方式如下:

  1. kitten → sitten(将k改为s)
  2. sitten → sittin(将e改为i)
  3. sittin → sitting(最后加入g)

题目的编辑距离,就是俄国科学家Levenshtein 提出的。 共有三种变换方式,插入一个字符,删除一个字符,和替换一个字符。

难点就在于它对字符串的顺序有依赖,遍历处理的时候,不确定是插入还是删除、替换。

  看完了算是理解了题意,怎么实现还是一脸茫然,还是看看大神的文章吧。grandyang 介绍了递归+动态规划的两种方式。

这也是一个常规的思路,如果你之前不了解这个,没学过也没刷过题目,很难上来就贴出来一个这样推导过程,看没有注释的代码也觉得费劲。

递归

  我们从尾开始遍历字符串,对于当前比较的两个字符 word1[m] 和 word2[n],如果相同则跳过,不相同的情况,分上面的三种情况处理

 如果:word1[m]==word2[n-1],那我们就在word1[m] 后面插入word2[n]即可。(插入

如果:word1[m-1]== word2[n],那我们就删除word1[m]。(删除

如果:word1[m-1]== word2[n-1],那我们就把word1[m]替换word2[n] (替换

//递归
	public static int helper(String word1, String word2,int m,int n){
		
		//conner case:word1为 空,全部插入word2
		if(m==0){
			return n;
		}
		//word2为空:全部插入word1
		if(n== 0){
			return m ;
		}
		//相同跳过
		if(word1.charAt(m-1)==word2.charAt(n-1) ){
			return helper( word1,word2,m-1,n-1);
		}else{
			//插入:比较n-1
			int insert = helper(word1,word2,m,n-1);
			//删除:比较m-1
			int delete = helper(word1,word2,m-1,n );
			//替换:比较m-1,n-1
			int replace = helper(word1,word2,m-1,n-1 );
			return 1+Math.min(replace , Math.min(insert, delete));
		}
	}

当然,这样提交会不过的,TLE错误,需要加缓存数组来提高效率。接着看个别的实现方式。

动态规划

通常对于字符串的问题,能用递归的也能用动态规划,效率跟递归+缓存是一样的。

那么动态规划的三个问题:dp数组的含义,初始化,及状态转移方程。解析如下:

我们定义一个二维的数组 dp,其大小为 mxn,m和n分别为 word1 和 word2 的长度。dp[i][j] 表示从  word1[0..i-1]转换为word2[0..j-1]所需要的步骤。

初始化:  考虑边界情况,如果i=0,也就是word1=“”,那么将word1转化为word2的最少操作数为j(插入word2)
同理,如果j=0,也就是word2=“”,那么将word2转化为word1的最少操作数为i(插入word1).

状态转换:看个简单例子,如 word1 是 "bbc",word2 是 "abcd",可以得到 dp 数组如下:

  Ø a b c d
Ø 0 1 2 3 4
b 1 1 1 2 3
b 2 2 1 2 3
c 3 3 2 1 2

可以发现,当 word1[i] == word2[j] 时,dp[i][j] = dp[i - 1][j - 1]。从直观语义的角度上来看,就是相同可以跳过,不做操作。

word1[i-1]!=word2[j-1]

  跟上面递归一样,考虑不同情况。假设现在word1字母为a,word2字母为b

  1. 如果将a替换成b,则dp[i][j]=dp[i-1][j-1]+1
  2. 如果在a后面添加一个b,则dp[i][j]=dp[i][j-1]+1
  3. 如果将a删除,则dp[i][j]=dp[i-1][j]+1

这里需要理解,解释一下:如果插入即可使word1[0...i-1]转化为word2[0...j-1],也就意味着word1[0...i-1]可以转换为word2[0...j-2]。只要在此基础上再进行一次插入操作即可以完成转换。所以dp[i][j]=dp[i][j-1]+1;

参见上面的图,是斯坦福学校的教学材料。

public static void main(String[] args) {
		// TODO Auto-generated method stub
		int res = minDistance("horse","ros");
		System.out.println( res);
		int res1 = minDistance("intention","execution");
		System.out.println( res1);
	}
	/**
	 * dp: dp[i][j]含义 word1 的前i个字符转换到 word2 的前j个字符所需要的步骤
	 * dp初始化:第一行和第一列对应的值为字符串的长度(默认前面有个空串)
	 * 状态转移方程:
	 */
	public static int minDistance(String word1, String word2) {
		
		int m = word1.length();int n = word2.length();
		//数组
		int[][] dp = new int[m+1][n+1];
		//初始化第一列,word2 为空
		for(int i=0;i<=m;i++ ){
			dp[i][0] = i;
		}
		//初始化第一行:word1为空
		for(int i=0;i<=n;i++ ){
			dp[0][i] = i;
		}
		//状态转移
		for(int i=1;i<=m;i++){
			for(int j=1;j<=n;j++){
				//相同跳过
				if(word1.charAt(i-1)== word2.charAt(j-1)){
				  dp[i][j] = dp[i-1][j-1];	
				}
				else{//分表对应:替换,插入,删除
					dp[i][j] = Math.min( dp[i-1][j-1], Math.min(dp[i][j-1] , dp[i-1][j]))+1;
				}
			}
		}
		
		return dp[m][n] ;
    }

Runtime: 6 ms, faster than 56.53% of Java online submissions for Edit Distance.

Memory Usage: 36.5 MB, less than 100.00% of Java online submissions for Edit Distance.

时间复杂度O(M*N).

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值