字符串相似度算法 -- levenshtein distance 編輯距離算法

 


   文本比較的核心就是比較兩個給定的文本(可以是字節流等)之間的差異。目前,主流的比較文本之間的差異主要有兩大類。一類是基於編輯距離(Edit Distance)的,例如LD算法。一類是基於最長公共子串的(Longest Common Subsequence),例如Needleman/Wunsch算法等。

  LD算法(Levenshtein Distance)又成爲編輯距離算法(Edit Distance)。他是以字符串A通過插入字符、刪除字符、替換字符變成另一個字符串B,那麼操作的過程的次數表示兩個字符串的差異.

    基於動態規劃的算法是四類方法中發展最早的一種。1980 年SELLERS【SELLERS, P. 1980】將動態規劃的思想引入近似串匹配算法中,其時間複雜度爲O(mn),算法效率比較低但是能適應各種不同的距離函數。下面首先看如何計算編輯距離,然後講述如何將其應用到匹配算法中

假設我們要計算串x和串y的編輯距離ld(x,y),|x|=m,|y|=n。計算過程需要用到動態規劃矩陣C[0…|x|,0…|y|],C[i,j]表示要匹配串x[1…i]和串y[1…j]需要的操作的次數即串x[1…I]和y[1…j]的編輯距離。

 初始化如下:

C[i,0]=i

C[0,j]=j

計算公式如下:

    如果(x[i]==y[j])

C[i,j]= C[i-1,j-1]     

  否則

    1+min(C[i-1,j],C[i,j],C[i-1,j-1])

C[i,0]表示串x[1…I]與空串的編輯距離,同理,C[0,j]表示y[1…j]與空串的編輯距離。當計算C[i,j]時,所有C[i’,j’](i’<=i並且j’<=j)即C矩陣中所有C[i,j]左上方的單元都已計算完畢。

比較字符x[i]和字符Y[j],

如果x[i]==y[j],我們不再需要任何操作即可將x[1…i]轉化爲y[1…j];

否則,我們需要進行某種操作。可能的操作有下面三種:

l  刪除x[1…I]的最後一個字符x[i]並利用x[1…i-1]轉化爲y[1…j]的結果;

l  或者在x[1…i]末尾插入字符y[j]並利用x[1…i-1]轉化爲y[1…j-1]的結果;

l  或者將x[I]替換爲y[j]並利用x[i…I-1]轉化爲y[1…j-1]的結果。

計算公式又可表示爲

C[i,j]=min(C[i-1,j-1]+ δ(x[i],y[j]),C[i,j-1]+1,C[I-1,j]+1)。

如果x[i]==y[j]:    δ(x[i],y[j])=0,

如果x[i]!=y[j]:    δ(x[i],y[j])= 1 。

    其數據依賴關係爲:動態規劃矩陣中的每一個元素,依賴於它左側、上方、左上方的元素。

在計算相似度矩陣時,需要計算整個(n+1)×(m+1)表格的每一項。在計算任意一項C[i, j]時,需要藉助於(i-1, j-1)、(i, j-1)和(i-1,j)這三項的分值因此,計算相似度矩陣算法的複雜度爲c×(n+1)×(m+1) = O(nm),c爲一常量。動態規劃算法的時間複雜度O(mn),空間複雜度O(min(m,n))。

例如:

   許多程序會大量使用字符串。對於不同的字符串,我們希望能夠有辦法判斷其相似程度。我們定義了一套操作方法來把兩個不相同的字符串變得相同,具體的操作方法爲:

    1.修改一個字符(如把“a”替換爲“b”)。

    2.增加一個字符(如把“abdd”變爲“aebdd”)。

    3.刪除一個字符(如把“travelling”變爲“traveling”)。

  比如,對於“abcdefg”和“abcdef”兩個字符串來說,我們認爲可以通過增加/減少一個“g“的方式來達到目的。上面的兩種方案,都僅需要一次操作。把這個操作所需要的次數定義爲兩個字符串的距離,給定任意兩個字符串,你是否能寫出一個算法來計算出它們的距離?

  分析與解法

  不難看出,兩個字符串的距離肯定不超過它們的長度之和(我們可以通過刪除操作把兩個串都轉化爲空串)。雖然這個結論對結果沒有幫助,但至少可以知道,任意兩個字符串的距離都是有限的。

  我們還是應該集中考慮如何才能把這個問題轉化成規模較小的同樣的問題。如果有兩個串A=xabcdae和B=xfdfa,它們的第一個字符是相同的,只要計算A[2,…,7]=abcdae和B[2,…,5]=fdfa的距離就可以了。但是如果兩個串的第一個字符不相同,那麼可以進行如下的操作(lenA和lenB分別是A串和B串的長度):

    1.刪除A串的第一個字符,然後計算A[2,…,lenA]和B[1,…,lenB]的距離。

    2.刪除B串的第一個字符,然後計算A[1,…,lenA]和B[2,…,lenB]的距離。

    3.修改A串的第一個字符爲B串的第一個字符,然後計算A[2,…,lenA]和B[2,…,lenB]的距離。

    4.修改B串的第一個字符爲A串的第一個字符,然後計算A[2,…,lenA]和B[2,…,lenB]的距離。

    5.增加B串的第一個字符到A串的第一個字符之前,然後計算A[1,…,lenA]和B[2,…,lenB]的距離。

    6.增加A串的第一個字符到B串的第一個字符之前,然後計算A[2,…,lenA]和B[1,…,lenB]的距離。

  在這個題目中,我們並不在乎兩個字符串變得相等之後的字符串是怎樣的。所以,可以將上面6個操作合併爲:

    1.一步操作之後,再將A[2,…,lenA]和B[1,…,lenB]變成相同字符串。

    2.一步操作之後,再將A[1,…,lenA]和B[2,…,lenB]變成相同字符串。

    3.一步操作之後,再將A[2,…,lenA]和B[2,…,lenB]變成相同字符串。

  這樣,很快就可以完成一個遞歸程序。

  在以上面的思想完成代碼後,對程序進行了一番測試。第一次找了兩個相似的字符串,長度分別爲15和17。速度和結果都比較滿意。這也印證了算法的正確性。第二次找了兩個相似的字符串,長度分別爲1500和1507。嗯,直接跳出錯誤,說是堆棧錯誤。實際上是由於遞歸嵌套出了問題。採用遞歸算法,只是理論上有效,便於理解,實際應用中會出現各種限制。如本例,嵌套約1000層的時候就超過了系統的限制。必須想一個解決之道。仔細觀察,可以發現用數學性的語言描述就是

  F(n,m)=G(F(n,m),F(n+1,m),F(n,m+1))

  這個可以簡化爲遞推,由於遞推可以放在一個函數內,就解決了系統的遞歸限制。

1.百度百科介紹:

Levenshtein 距離,又稱編輯距離,指的是兩個字符串之間,由一個轉換成另一個所需的最少編輯操作次數。

許可的編輯操作包括將一個字符替換成另一個字符,插入一個字符,刪除一個字符。

編輯距離的算法是首先由俄國科學家Levenshtein提出的,故又叫Levenshtein Distance。

2.用途

模糊查詢

3.實現過程

a.首先是有兩個字符串,這裏寫一個簡單的 abc和abe

b.將字符串想象成下面的結構。

A 是一個標記,爲了方便講解,不是這個表的內容。

  abc a b c
abe 0 1 2 3
a 1

A

   
b 2      
e 3      

 

c.來計算A 出得值

它的值取決於:左邊的1、上邊的1、左上角的0.

按照Levenshtein distance的意思:

上面的值和左面的值都要求加1,這樣得到1+1=2。

A 由於是兩個a相同,左上角的值加0.這樣得到0+0=0。

這是後有三個值,左邊的計算後爲2,上邊的計算後爲2,左上角的計算爲0,所以A 取他們裏面最小的0.

d.於是表成爲下面的樣子

  abc a b c
abe 0 1 2 3
a 1

0

   
b 2

A

   
e 3      

B會同樣得到三個值,左邊計算後爲3,上邊計算後爲1,在B處 由於對應的字符爲a、b,不相等,所以左上角應該在當前值的基礎上加1,這樣得到1+1=2,在(3,1,2)中選出最小的爲B處的值。

e.於是表就更新了

  abc a b c
abe 0 1 2 3
a 1

0

   
b 2

1

   
e 3

C

   

 

C 計算後:上面的值爲2,左邊的值爲4,左上角的:a和e不相同,所以加1,即2+1,左上角的爲3。

在(2,4,3)中取最小的爲C 的值。

f.於是依次推得到

I處: 表示abc 和abe 有1個需要編輯的操作。這個是需要計算出來的。

同時,也獲得一些額外的信息。

A處: 表示a 和a 需要有0個操作。字符串一樣

B處: 表示ab 和a 需要有1個操作。

C處: 表示abe 和a 需要有2個操作。

D處: 表示a 和ab 需要有1個操作。

E處: 表示ab 和ab 需要有0個操作。字符串一樣

F處: 表示abe 和ab 需要有1個操作。

G處: 表示a 和abc 需要有2個操作。

H處: 表示ab 和abc 需要有1個操作。

I處: 表示abe 和abc 需要有1個操作。

g.計算相似度

先取兩個字符串長度的最大值maxLen,用1-(需要操作數除/maxLen),得到相似度。

例如abc 和abe一個操作,長度爲3,所以相似度爲1-1/3=0.666。

4.代碼實現計算編輯距離

直接能運行, 複製過去就行。

intMinimum (int a, int b, int c)

{

       int mi;  

       mi = a;

       if (b < mi) {

              mi = b;

       }

       if (c < mi) {

              mi = c;

       }

       return mi;

      

}

 

voidlevenshteinDistance(char * x,char *y)

{

       int i,j,m,n;

       int dsw[strlen(x)][strlen(y)];

       m=strlen(x);

       n=strlen(y);

       for(i=0;i<=m;i++)

              dsw[i][0]=i; 

       for(j=1;j<=n;j++)

              dsw[0][j]=j;

       for(i=1;i<=m;i++){

              for(j=1;j<=n;j++){

                     if(x[i]==y[j])

                            dsw[i][j]=dsw[i-1][j-1];

                     else 

                            dsw[i][j]=1+Minimum(dsw[i-1][j-1],dsw[i-1][j],dsw[i][j-1]);     }

       }

       for(i=1;i<=m;i++){

              for(j=1;j<=n;j++){

                     printf("%4d,",dsw[i][j]);

              }

              printf("\n");

       }

}

intmain(){

       levenshteinDistance("ncicict","casnciccn");

}
結果:同上
   1,   2,   3,   3,   4,   5,   6,   7,   8,
   2,   2,   3,   4,   3,   4,   5,   6,   7,
   3,   3,   3,   3,   4,   3,   4,   5,   6,
   4,   4,   4,   4,   3,   4,   4,   5,   6,
   5,   5,   5,   4,   4,   3,   4,   5,   6,
   6,   6,   6,   5,   5,   4,   4,   5,   6,
   7,   7,   7,   6,   6,   5,   5,   5,   5,
package code;
 
/**
 * @className:MyLevenshtein.java
 * @classDescription:Levenshtein Distance 算法實現
 * 可以使用的地方:DNA分析   拼字檢查   語音辨識   抄襲偵測
 * @author:donghai.wan
 * @createTime:2012-1-12
 */
public class MyLevenshtein {
 
  public static void main(String[] args) {
   //要比較的兩個字符串
   String str1 = "今天星期四";
   String str2 = "今天是星期五";
   levenshtein(str1,str2);
  }
 
  /**
   *   DNA分析   拼字檢查   語音辨識   抄襲偵測
   * 
   * @createTime 2012-1-12
   */
  public static void levenshtein(String str1,String str2) {
   //計算兩個字符串的長度。
   int len1 = str1.length();
   int len2 = str2.length();
   //建立上面說的數組,比字符長度大一個空間
   int[][] dif = new int[len1 + 1][len2 + 1];
   //賦初值,步驟B。
   for (int a = 0; a <= len1; a++) {
     dif[a][0] = a;
   }
   for (int a = 0; a <= len2; a++) {
     dif[0][a] = a;
   }
   //計算兩個字符是否一樣,計算左上的值
   int temp;
   for (int i = 1; i <= len1; i++) {
     for (int j = 1; j <= len2; j++) {
      if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
        temp = 0;
      } else {
        temp = 1;
      }
      //取三個值中最小的
      dif[i][j] = min(dif[i - 1][j - 1] + temp, dif[i][j - 1] + 1,
         dif[i - 1][j] + 1);
     }
   }
   System.out.println("字符串\""+str1+"\"與\""+str2+"\"的比較");
   //取數組右下角的值,同樣不同位置代表不同字符串的比較
   System.out.println("差異步驟:"+dif[len1][len2]);
   //計算相似度
   float similarity =1 - (float) dif[len1][len2] / Math.max(str1.length(), str2.length());
   System.out.println("相似度:"+similarity);
  }
 
  //得到最小值
  private static int min(int... is) {
   int min = Integer.MAX_VALUE;
   for (int i : is) {
     if (min > i) {
      min = i;
     }
   }
   return min;
  }
 
}

5.舉例

首先在連續相等的字符就可以考慮到

紅色是取值的順序。

1.今天週一 天週一

實現是去掉“今”,一步完成。

2.聽說馬上就要放假了 你聽說要放假了

這兩個字符串是:

去掉“你”,加上“馬上就”,總共四步操作。

6. 得到匹配結果

我們往往不僅僅是計算出字符串A和字符串B的編輯距離,還要能得出他們的匹配結果。

  以例A=GGATCGA,B=GAATTCAGTTA,LD(A,B)=5

  他們的匹配爲:

AGGA_TC_G__A

    BGAATTCAGTTA

  如上面所示,藍色表示完全匹配,黑色表示編輯操作_表示插入字符或者是刪除字符操作。如上面所示,黑色字符有5個,表示編輯距離爲5。

利用LD矩陣,通過回溯,能找到匹配字串

第一步:定位在矩陣的右下角  

LD算法矩陣

 

b

G

A

A

T

T

C

A

G

T

T

A

a

0

1

2

3

4

5

6

7

8

9

10

11

G

1

0

1

2

3

4

5

6

7

8

9

10

G

2

1

1

2

3

4

5

6

6

7

8

9

A

3

2

1

1

2

3

4

5

6

7

8

8

T

4

3

2

2

1

2

3

4

5

6

7

8

C

5

4

3

3

2

2

2

3

4

5

6

7

G

6

5

4

4

3

3

3

3

3

4

5

6

A

7

6

5

4

4

4

4

3

4

4

5

5

  第二步:回溯單元格,至矩陣的左上角

    若ai=bj,則回溯到左上角單元格

LD算法矩陣

 

b

G

A

A

T

T

C

A

G

T

T

A

 a

0

1

2

3

4

5

6

7

8

9

10

11

G

1

0

1

2

3

4

5

6

7

8

9

10

G

2

1

1

2

3

4

5

6

6

7

8

9

A

3

2

1

1

2

3

4

5

6

7

8

8

T

4

3

2

2

1

2

3

4

5

6

7

8

C

5

4

3

3

2

2

2

3

4

5

6

7

G

6

5

4

4

3

3

3

3

3

4

5

6

A

7

6

5

4

4

4

4

3

4

4

5

5

    若ai≠bj,回溯到左上角、上邊、左邊中值最小的單元格,若有相同最小值的單元格,優先級按照左上角、上邊、左邊的順序

LD算法矩陣

 

b

G

A

A

T

T

C

A

G

T

T

A

a

0

1

2

3

4

5

6

7

8

9

10

11

G

1

0

1

2

3

4

5

6

7

8

9

10

G

2

1

1

2

3

4

5

6

6

7

8

9

A

3

2

1

1

2

3

4

5

6

7

8

8

T

4

3

2

2

1

2

3

4

5

6

7

8

C

5

4

3

3

2

2

2

3

4

5

6

7

G

6

5

4

4

3

3

3

3

3

4

5

6

A

7

6

5

4

4

4

4

3

4

4

5

5

    若當前單元格是在矩陣的第一行,則回溯至左邊的單元格

    若當前單元格是在矩陣的第一列,則回溯至上邊的單元格

LD算法矩陣

 

b

G

A

A

T

T

C

A

G

T

T

A

a

0

1

2

3

4

5

6

7

8

9

10

11

G

1

0

1

2

3

4

5

6

7

8

9

10

G

2

1

1

2

3

4

5

6

6

7

8

9

A

3

2

1

1

2

3

4

5

6

7

8

8

T

4

3

2

2

1

2

3

4

5

6

7

8

C

5

4

3

3

2

2

2

3

4

5

6

7

G

6

5

4

4

3

3

3

3

3

4

5

6

A

7

6

5

4

4

4

4

3

4

4

5

5

    依照上面的回溯法則,回溯到矩陣的左上角

  第三步:根據回溯路徑,寫出匹配字串

   若回溯到左上角單元格,將ai添加到匹配字串A,將bj添加到匹配字串B

   若回溯到上邊單元格,將ai添加到匹配字串A,將_添加到匹配字串B

   若回溯到左邊單元格,將_添加到匹配字串A,將bj添加到匹配字串B

   搜索晚整個匹配路徑,匹配字串也就完成了

  從上面可以看出,LD算法在不需要計算出匹配字串的話,時間複雜度爲O(MN),空間複雜度經優化後爲O(M)

  不過,如果要計算匹配字符串的話,時間複雜度爲O(MN),空間複雜度由於需要利用LD矩陣計算匹配路徑,故空間複雜度仍然爲O(MN)。這個在兩個字符串都比較短小的情況下,能獲得不錯的性能。不過,如果字符串比較長的情況下,就需要極大的空間存放矩陣。例如:兩個字符串都是20000字符,則LD矩陣的大小爲20000*20000*2=800000000Byte=800MB。呵呵,這是什麼概念?

 

參考

戴正華《串匹配算法》

http://www.cnitblog.com/ictfly/archive/2005/12/27/5828.aspx

http://www.cnblogs.com/grenet/category/287355.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章