字符串差異比較算法通常應(yīng)用于比較同一個(gè)文件的不同版本,或者兩篇雷同的文章的相同部分。下面將問題描述如下:
給定兩個(gè)字符串a(chǎn),b。編寫程序?qū)與b相同的部分用空格替代,不同的部分保留。
舉例說明:設(shè)a=“一輩子只做一件事”,b=“生來只做一件事”。經(jīng)過處理后,字符串a(chǎn)變?yōu)椤耙惠呑? ”,字符串b變?yōu)椤吧鷣? ”。
經(jīng)過如上處理后,我們僅需要將不為空格部分的高亮就可以對(duì)兩個(gè)文本的不同一目了然了。
上面的問題,比較直接的思路是將兩個(gè)字符串拆分為字符的集合,求兩個(gè)字符集合的交集。然后將在交集中的字符替換為空格。但并不能完美解決問題。比如,字符串a(chǎn)中“一”出現(xiàn)了兩次,而b中出現(xiàn)了一次。程序會(huì)誤將a中的兩個(gè)“一”都當(dāng)做相同部分。從而得到a=“ 輩子 ”。
產(chǎn)生上面問題的原因是單個(gè)字太容易在字符串中重復(fù)了,這就讓我們考慮把字符串分解為長度為2的子串。比如a分解為:一輩,輩子,子只,只做,做一,一件,件事
。而b分解為:生來,來只,只做,做一,一件,件事
。可以看出,兩個(gè)字符串都有只做,做一,一件,件事
。去掉相同部分后可以得到正確的結(jié)果。
然而,當(dāng)文章很長時(shí),長度為二的子串也會(huì)產(chǎn)生重復(fù)。這就提示我們要從最長的子串開始來找重復(fù)項(xiàng)。
解決思路:假設(shè)a、b中較短的字符串為a,假設(shè)其長度為$l_a$,則:
- l=$l_a$
- 窮舉字符串長度為l的子串,針對(duì)每個(gè)子串看字符串b是否包含該子串
- 若包含則將a,b相應(yīng)部分設(shè)置為空格。a中空格區(qū)域左邊若不為空串,則與b進(jìn)行進(jìn)一步遞歸比較。同理,a中空格區(qū)域右邊若不為空串,則與b進(jìn)行進(jìn)一步遞歸比較。(分為左右遞歸是為了窮舉a的子串時(shí)避開已經(jīng)設(shè)置為空串的部分),直接返回結(jié)果。
- 2步驟中的所有子串b都不包含,則l=l/2
注意:當(dāng)a與b比較相似時(shí),該算法的效率較高。由于l從最大可能的子串開始窮舉,其可以很快將大量相同部分設(shè)置為空格,從而減少后續(xù)窮舉子串的計(jì)算量。若a與b幾乎完全不同,則窮舉加比較的復(fù)雜度就會(huì)正比于a的字符數(shù)*b的字符數(shù)
。
算法的關(guān)鍵部分如下(java語言描述),算法的入口是第一個(gè)getDiff方法。
//獲取兩個(gè)字符串的差異,將相同部分設(shè)置為空格,返回的字符串?dāng)?shù)組為處理后的結(jié)果
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進(jìn)行比較生成比對(duì)結(jié)果
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) {
//遞歸獲取空白區(qū)域左邊差異
result = getDiff(result[0], result[1], 0, i);
}
if (i + len < end) {
//遞歸獲取空白區(qū)域右邊差異
result = getDiff(result[0], result[1], i + len, end);
}
len=0;//退出while循環(huán)
break;
}
}
len = len / 2;
}
return result;
}
//將字符串s指定的區(qū)域設(shè)置成空格
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);
}