首先聲明:網上關於Needleman/Wunsch算法有很很多,也有更詳細的介紹,但是關於用JAVA如何去實現兩段文本字符串的比較就比較少,本文是結合大量同行的的代碼的同時加以總結,希望能對後來閱讀的人有所幫助,若對某哥的代碼有所抄襲,請見諒:
part 1:Needleman/Wunsch算法介紹
本文介紹基於最長公共子串的文本比較算法——Needleman/Wunsch算法。
還是以實例說明:字符串A=kitten,字符串B=sitting
那他們的最長公共子串爲ittn(注:最長公共子串不需要連續出現,但一定是出現的順序一致),最長公共子串長度爲4。
定義:
LCS(A,B)表示字符串A和字符串B的最長公共子串的長度。很顯然,LSC(A,B)=0表示兩個字符串沒有公共部分。
Rev(A)表示反轉字符串A
Len(A)表示字符串A的長度
A+B表示連接字符串A和字符串B
性質:
LCS(A,A)=Len(A)
LCS(A,"")=0
LCS(A,B)=LCS(B,A)
0≤LCS(A,B)≤Min(Len(A),Len(B))
LCS(A,B)=LCS(Rev(A),Rev(B))
LCS(A+C,B+C)=LCS(A,B)+Len(C)
LCS(A+B,A+C)=Len(A)+LCS(B,C)
LCS(A,B)≥LCS(A,C)+LCS(B,C)
LCS(A+C,B)≥LCS(A,B)+LCS(B,C)
爲了講解計算LCS(A,B),特給予以下幾個定義
A=a1a2……aN,表示A是由a1a2……aN這N個字符組成,Len(A)=N
B=b1b2……bM,表示B是由b1b2……bM這M個字符組成,Len(B)=M
定義LCS(i,j)=LCS(a1a2……ai,b1b2……bj),其中0≤i≤N,0≤j≤M
故: LCS(N,M)=LCS(A,B)
LCS(0,0)=0
LCS(0,j)=0
LCS(i,0)=0
對於1≤i≤N,1≤j≤M,有公式一
若ai=bj,則LCS(i,j)=LCS(i-1,j-1)+1
若ai≠bj,則LCS(i,j)=Max(LCS(i-1,j-1),LCS(i-1,j),LCS(i,j-1))
計算LCS(A,B)的算法有很多,下面介紹的Needleman/Wunsch算法是其中的一種。和LD算法類似,Needleman/Wunsch算法用的都是動態規劃的思想。在Needleman/Wunsch算法中還設定了一個權值,用以區分三種操作(插入、刪除、更改)的優先級。在下面的算法中,認爲三種操作的優先級都一樣。故權值默認爲1。
舉例說明:A=GGATCGA,B=GAATTCAGTTA,計算LCS(A,B)
第一步:初始化LCS矩陣
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | |||||||||||
G | 0 | |||||||||||
A | 0 | |||||||||||
T | 0 | |||||||||||
C | 0 | |||||||||||
G | 0 | |||||||||||
A | 0 |
第二步:利用公式一,計算矩陣的第一行
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
G | 0 | |||||||||||
A | 0 | |||||||||||
T | 0 | |||||||||||
C | 0 | |||||||||||
G | 0 | |||||||||||
A | 0 |
第三步:利用公式一,計算矩陣的其餘各行
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 |
A | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
T | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
C | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
G | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 5 |
A | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 6 |
則,LCS(A,B)=LCS(7,11)=6
可以看出,Needleman/Wunsch算法實際上和LD算法是非常接近的。故他們的時間複雜度和空間複雜度也一樣。時間複雜度爲O(MN),空間複雜度爲O(MN)。空間複雜度經過優化,可以優化到O(M),但是一旦優化就喪失了計算匹配字串的機會了。由於代碼和LD算法幾乎一樣。這裏就不再貼代碼了。
還是以上面爲例A=GGATCGA,B=GAATTCAGTTA,LCS(A,B)=6
他們的匹配爲:
A:GGA_TC_G__A
B:GAATTCAGTTA
如上面所示,藍色表示完全匹配,黑色表示編輯操作,_表示插入字符或者是刪除字符操作。如上面所示,藍色字符有6個,表示最長公共子串長度爲6。
利用上面的Needleman/Wunsch算法矩陣,通過回溯,能找到匹配字串
第一步:定位在矩陣的右下角
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 |
A | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
T | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
C | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
G | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 5 |
A | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 6 |
第二步:回溯單元格,至矩陣的左上角
若ai=bj,則回溯到左上角單元格
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 |
A | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
T | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
C | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
G | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 5 |
A | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 6 |
若ai≠bj,回溯到左上角、上邊、左邊中值最大的單元格,若有相同最大值的單元格,優先級按照左上角、上邊、左邊的順序
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 |
A | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
T | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
C | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
G | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 5 |
A | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 6 |
若當前單元格是在矩陣的第一行,則回溯至左邊的單元格
若當前單元格是在矩陣的第一列,則回溯至上邊的單元格
G | A | A | T | T | C | A | G | T | T | A | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
G | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 |
A | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
T | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
C | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
G | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 5 |
A | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 4 | 5 | 5 | 5 | 6 |
依照上面的回溯法則,回溯到矩陣的左上角
第三步:根據回溯路徑,寫出匹配字串
若回溯到左上角單元格,將ai添加到匹配字串A,將bj添加到匹配字串B
若回溯到上邊單元格,將ai添加到匹配字串A,將_添加到匹配字串B
若回溯到左邊單元格,將_添加到匹配字串A,將bj添加到匹配字串B
搜索晚整個匹配路徑,匹配字串也就完成了
可以看出,LD算法和Needleman/Wunsch算法的回溯路徑是一樣的。這樣找到的匹配字串也是一樣的。
不過,Needleman/Wunsch算法和LD算法一樣,若要找出匹配字串,空間的複雜度就一定是O(MN),在文本比較長的時候,是極爲耗用存儲空間的。故若要計算出匹配字串,還得用其他的算法,留待後文介紹。
part 2:Java實現
-
首先了解一下這個算法,這是動態規劃中的一個經典例子,對於遞歸的概念的深刻理解,將一個大問題變成小問題,並逐步求解。由第一個字符,我們假設爲缺失開始,每一加一個字符,都有三種情況,mismatch,match,deletion/insert,對應的進行打分。高分值的爲最優解,逐步延伸至最後一個字符。如有不懂,可以對這個模型在進行深刻理解。
-
該代碼對回溯步驟進行簡化,僅返回一個最優解,但是該方法簡單,適合初級編程者,自學所用。
源代碼:
/*Needleman Wunsch是一個全局比對算法,可用於DNA和蛋白質序列的全局比對*/
public class Needleman_Wunsch {
/*全局變量用於回溯是的指針*/
static int l=0;
public static void main(String[] args) {
/*比對的兩列字符串*/
String t="GCGCAATG";
String p ="GCCCTAGCG";
/*創建H矩陣用於打分,成爲打分矩陣,創建D矩陣用於回溯,成爲指針矩陣或者方向矩陣*/
int tlen=t.length();
int plen=p.length();
int [][] h=new int[tlen+1][plen+1];
int [][] d=new int[tlen+1][plen+1];
/*初始化矩陣,第一列或行爲deletion後者insert,扣分2*/
for(int i=0;i<1;i++){
for(int j=0;j<plen+1;j++){
h[i][j]=-2*j;
d[i][j]=3;
}
}
for(int j=0;j<1;j++){
for(int i=0;i<tlen+1;i++){
h[i][j]=-2*i;
d[i][j]=1;
}
}
/*動態規劃用於打分*/
for(int i=1;i<tlen+1;i++){
for(int j=1;j<plen+1;j++){
/*分值:mismatch(失配)-1,deletion(缺失)/inserting(插入)-2,match(匹配)1,*/
int s1=-2,s2=0,s3=-2;
if(t.charAt(i-1)==p.charAt(j-1)){
s2=1;
}else{
s2=-1;
}
h[i][j]=maximum(h[i-1][j]+s1,h[i-1][j-1]+s2,h[i][j-1]+s3);
d[i][j]=l;
}
}
/*輸出打分矩陣*/
System.out.println("score matrix:");
for(int i=0;i<tlen+1;i++){
for(int j=0;j<plen+1;j++){
System.out.printf("%4d",h[i][j]);
if(j!=0&&j%plen==0){
System.out.println();
}
}
}
/*輸出索引矩陣*/
System.out.println("index matrix:");
for(int i=0;i<tlen+1;i++){
for(int j=0;j<plen+1;j++){
System.out.print(d[i][j]+" ");
if(j!=0&&j%plen==0){
System.out.println();
}
}
}
/*輸出結果*/
System.out.print("Target sequence:");
String result =get_back(t,p,d);
for (int i=0;i<result.length();i++){
System.out.print(result.charAt(i)+" ");
}
System.out.println();
System.out.print("Source sequence:");
for (int i=0;i<p.length();i++){
System.out.print(p.charAt(i)+" ");
}
}
/*求最大值的方法*/
public static int maximum(int a,int b,int c){
int max =a;
l=1;
if(a<b){
max=b;
l=2;
if(b<c){
max=c;
l=3;
}
}else if(a<c){
max=c;
l=3;
}
if(max==a&&max==b){
l=4;
}else if(max==a&&max==c){
l=5;
}else if(max==b&&max==c){
l=6;
}
if(max==a&&max==b&&max==c){
l=7;
}
return max;
}
/*回溯方法*/
public static String get_back(String t,String p,int[][] d){
int i=t.length();
int j=p.length();
StringBuffer sb = new StringBuffer();
while(i>=0&&j>0){
int start = d[i][j];
switch(start){
case 1:sb.insert(0, t.charAt(i-1));i=i-1;break;
case 2:sb.insert(0, t.charAt(i-1));i=i-1;j=j-1;break;
case 3:sb.insert(0, '-');j=j-1;break;
case 4:sb.insert(0, t.charAt(i-1));i=i-1;j=j-1;break;
case 5:sb.insert(0, t.charAt(i-1));i=i-1;break;
case 6:sb.insert(0, t.charAt(i-1));i=i-1;j=j-1;break;
case 7:sb.insert(0, t.charAt(i-1));i=i-1;j=j-1;break;
}
}
String result =sb.toString();
return result;
}
}
-
結果分析:
Target sequence:G C G C - A A T G
Source sequence:G C C C T A G C G
此結果爲最優解之一。