字符串差异比较算法通常应用于比较同一个文件的不同版本,或者两篇雷同的文章的相同部分。下面将问题描述如下:
给定两个字符串a,b。编写程序将a与b相同的部分用空格替代,不同的部分保留。
举例说明:设a=“一辈子只做一件事”,b=“生来只做一件事”。经过处理后,字符串a变为“一辈子 ”,字符串b变为“生来 ”。
经过如上处理后,我们仅需要将不为空格部分的高亮就可以对两个文本的不同一目了然了。
上面的问题,比较直接的思路是将两个字符串拆分为字符的集合,求两个字符集合的交集。然后将在交集中的字符替换为空格。但并不能完美解决问题。比如,字符串a中“一”出现了两次,而b中出现了一次。程序会误将a中的两个“一”都当做相同部分。从而得到a=“ 辈子 ”。
产生上面问题的原因是单个字太容易在字符串中重复了,这就让我们考虑把字符串分解为长度为2的子串。比如a分解为:一辈,辈子,子只,只做,做一,一件,件事
。而b分解为:生来,来只,只做,做一,一件,件事
。可以看出,两个字符串都有只做,做一,一件,件事
。去掉相同部分后可以得到正确的结果。
然而,当文章很长时,长度为二的子串也会产生重复。这就提示我们要从最长的子串开始来找重复项。
解决思路:假设a、b中较短的字符串为a,假设其长度为$l_a$,则:
- l=$l_a$
- 穷举字符串长度为l的子串,针对每个子串看字符串b是否包含该子串
- 若包含则将a,b相应部分设置为空格。a中空格区域左边若不为空串,则与b进行进一步递归比较。同理,a中空格区域右边若不为空串,则与b进行进一步递归比较。(分为左右递归是为了穷举a的子串时避开已经设置为空串的部分),直接返回结果。
- 2步骤中的所有子串b都不包含,则l=l/2
注意:当a与b比较相似时,该算法的效率较高。由于l从最大可能的子串开始穷举,其可以很快将大量相同部分设置为空格,从而减少后续穷举子串的计算量。若a与b几乎完全不同,则穷举加比较的复杂度就会正比于a的字符数*b的字符数
。
算法的关键部分如下(java语言描述),算法的入口是第一个getDiff方法。
//获取两个字符串的差异,将相同部分设置为空格,返回的字符串数组为处理后的结果
public String[] getDiff(String a, String b) {
String[] result = null;
//选取长度较小的字符串用来穷举子串
if (a.length() < b.length()) {
result = getDiff(a, b, 0, a.length());
} else {
result = getDiff(b, a, 0, b.length());
result = new String[]{result[1],result[0]};
}
return result;
}
//将a的指定部分与b进行比较生成比对结果
private String[] getDiff(String a, String b, int start, int end){
String[] result = new String[]{a, b};
int len = result[0].length();
while (len > 0) {
for (int i = start; i < end - len + 1; i++) {
String sub = result[0].substring(i, i + len);
int idx = -1;
if ((idx = result[1].indexOf(sub)) != -1) {
System.out.println(sub);
result[0] = setEmpty(result[0], i, i + len);
result[1] = setEmpty(result[1], idx, idx + len);
if (i > 0) {
//递归获取空白区域左边差异
result = getDiff(result[0], result[1], 0, i);
}
if (i + len < end) {
//递归获取空白区域右边差异
result = getDiff(result[0], result[1], i + len, end);
}
len=0;//退出while循环
break;
}
}
len = len / 2;
}
return result;
}
//将字符串s指定的区域设置成空格
public String setEmpty(String s, int start, int end) {
char[] array = s.toCharArray();
for (int i = start; i < end; i++) {
array[i] = ' ';
}
return new String(array);
}