編輯距離——萊文斯坦距離(Levenshtein distance)

在信息論和計算機科學中,萊文斯坦距離是一種兩個字符串序列的距離度量。形式化地說,兩個單詞的萊文斯坦距離是一個單詞變成另一個單詞要求的最少單個字符編輯數量(如:刪除、插入和替換)。萊文斯坦距離也被稱做編輯距離,儘管它只是編輯距離的一種,與成對字符串比對緊密相關。

一、定義

數學上,兩個字符串a、b之間的萊文斯坦距離,levab(|a|, |b|)

levab(i, j) = max(i, j)  如果min(i, j) = 0;

                 =  min(levab(i - 1, j) + 1, levab(i, j-1) + 1, levab(i - 1, j - 1) + 1) (ai != bj)  否則

其中ai != bj 是指示函數,當ai != bj 是爲1, 否則爲0。注意在最小項:第一部分對應刪除操作(從a到b)、第二部分對應插入操作、第三部分對應替換操作。

二、例子

例如,“kitten"和”sitting"的編輯距離是3,因爲按照如下需3個字符編輯從源字符串到目標字符串且沒有比這種方式更少的編輯方式

1.kitten->sitten(用's'取代‘k')

2.sitten->sittin(用’i'取代’e')

3.sittin->sitting(在末尾插入’g')

三、上下界

萊文斯坦距離有幾個簡單的上下界,包括:

1.至少總是兩個字符串大小的差值;

2.至多是較長字符串的長度;

3.當且僅當兩個字符串相等時值爲0;

4.如果兩個字符串大小相等,漢明距離是其上界;

5.兩個字符串的萊文斯坦距離不大於分別與第三個字符串的萊文斯坦距離之和(三角不等式)。

四、應用

在字符串近似匹配中,目標是從很多長的的文本中發現短文本的匹配,在這種情況下,較少差別的匹配是期望得到的。例如,短文本可以來自字典,這裏,通常其中一個字符串是短的,另一個字符串是任意長的。萊文斯坦距離有着廣泛的應用,例如,拼寫檢查、光學字符的校正系統、基於翻譯記憶庫的自然語言翻譯的輔助軟件。

五、與其他編輯距離度量的關係

其他的流行的編輯距離度量,包括了不同的編輯操作。例如

Damerau–萊文斯坦距離(Damerau–Levenshtein distance)允許插入、刪除、替換和交換兩個相鄰字符;

最長公共子序列( longest common subsequence)只允許插入和刪除操作;

漢明距離(Hamming distance)只允許替換操作,因此只適用於兩個相等長度的字符串。

六、計算萊文斯坦距離

1、遞歸法

遞歸法計算萊文斯坦距離是直觀但效率比較差,重複計算相同的子序列很多次,有效的改進方式是構造一個距離表存儲每次計算的結果,最右下元素即爲兩個全字符串的levenshtein距離。

2.全矩陣迭代法

使用了自底向上的動態規劃實現方法,也即上面提到的構造距離表取代重複計算相同子序列,相對於遞歸實現除速度顯著改進外,算法運行時只需s_len * t_len的內存(s_len和t_len分別是字符串s和t的長度)

正確性證明:

算法的循環不變式是:可以轉換初始的s[1...i]到t[1...j]使用最少的d[i, j]個操作,循環不變式得到保持的原因:

(1)初始的正確性:0列,因爲初始的部分s[1..i]可以通過簡單的刪除所有i個字符轉換成空字符串t[1..0]。相似地,可以通過添加所有j個字符將空字符串s[1..0]轉換成t[1..j]。

(2)如果s(i) = t(j),並且可以通過k個操作將s[1...i-1]轉換成t[1..j-1]:可以對s[1..i-1]進行相同的操作,不對最後一個字符進行任何操作。

(3)否則,距離d[i, j]是三種可能轉換方式中的最小距離:

(a)如果可以在k個操作中將s[1..i]轉換成t[1..j-1],則可以在k+1個操作中通過簡單的添加t[j]以得到t[1..j](插入操作);

(b)如果可以在k個操作中將s[1...i-1]轉換成t[1...j],則可以在k+1個操作中移除s[i]之後做相同的轉換(刪除操作);

(c)如果可以在k個操作中將s[1...i-1]轉換成t[1..j-1],可以對s[1..i]做相同的操作,之後交換原先的s[i]和t[j],總共k+1個操作(替換操作)。

將s[1...m]轉換成t[1...n]所要求的操作理所當然的是轉換s的所有字符到t的所有字符,所以d[m][n]是最終的結果。

這個證明並不能驗證d[i][j]是實際的最小編輯距離,實際的證明過程比較困難,可以通過反證法進行證明。

3.兩行迭代法

若不想重構已經被編輯的輸入字符串,表明只要使用距離矩陣的兩行即可計算萊文斯坦編輯距離(前一行和當前行)。

七、算法實現

在實現的過程中將三種實現方式作爲一個源文件:

#include <stdio.h>

int levenshteinDistance(char[], int, char[], int);
int levenshteinDynamicProgramming(char[], int, char[], int);
int levenshteinTwoRows(char[], int, char[], int);

int main()
{
    char s[] = "sitting";
    char t[] = "kitten";
    int s_len = 7;
    int t_len = 6;
    int mindis;

    mindis = levenshteinDistance(s, s_len, t, t_len);
    printf("使用遞歸方法實現的萊文斯坦距離算法計算結果:%3d\n\n", mindis);
    mindis = levenshteinDynamicProgramming(s, s_len, t, t_len);
    printf("使用自底向上方式的動態規劃實現的萊文斯坦距離算法計算結果:%3d\n\n", mindis);
    mindis = levenshteinTwoRows(s, s_len, t, t_len);
    printf("使用矩陣兩行迭代法實現的萊文斯坦距離算法計算結果:%3d\n", mindis);
    return 0;
}

//求三個整數中的最小數
int Min(int x, int y, int z)
{
    if(x <= y && x <= z)
        return x;
    else if (y <= z)
        return y;
    else
        return z;
}

// levenshtein距離的遞歸實現
int levenshteinDistance(char s[], int s_len, char t[], int t_len)
{
    int cost;

    //基本情況,若字符串s和t的最小距離爲0,則返回其中的最大距離作爲編輯距離
    if (s_len == 0)
        return t_len;
    if (t_len == 0)
        return s_len;
    //測試s和t的各自最後一個字符是否匹配
    if (s[s_len - 1] == t[t_len - 1])
        cost = 0;
    else
        cost = 1;
    //使用公式,返回三者中的最小距離
    return Min(levenshteinDistance(s, s_len - 1, t, t_len) + 1,
               levenshteinDistance(s, s_len, t, t_len - 1) + 1,
               levenshteinDistance(s, s_len - 1, t, t_len - 1) + cost
               );
}

//levenshtein距離的自底向上方式的動態規劃實現,把重複計算的距離存入一個矩陣中
int levenshteinDynamicProgramming(char s[], int s_len, char t[], int t_len)
{
    //構建一個(s_len+1)*(t_len+1)的矩陣d,d[i][j]表示字符串s的前i字符和t的前j個字符的萊文斯坦距離
    int d[s_len+1][t_len+1];
    int i, j;

    //源字符串s到空字符串t只要刪除每個字符
    for (i = 0; i <= s_len; i++)
        d[i][0] = i;
    //從空字符s到目標字符t只要添加每個字符
    for (j = 1; j <= t_len; j++)
        d[0][j] = j;
    for (j = 0; j < t_len; j++)
        for (i = 0; i < s_len; i++)
            if (s[i] == t[j])
                d[i+1][j+1] = d[i][j]; //不進行任何操作
            else
                d[i+1][j+1] = Min(d[i][j+1] + 1,  //刪除操作
                              d[i+1][j] + 1,  //添加操作
                              d[i][j] + 1 //替換操作
                              );
    printf("使用自底向上方式動態規劃實現得到的編輯距離矩陣:\n");
    for (i = 0; i <= s_len; i++) {
        for (j = 0; j <= t_len; j++)
            printf("%3d", d[i][j]);
        printf("\n");
    }

    return d[s_len][t_len];
}

// evenshtein距離的兩列矩陣的迭代實現
int levenshteinTwoRows(char s[], int s_len, char t[], int t_len)
{
    //退化的基本情況
    if (s_len == 0)
        return t_len;
    if (t_len == 0)
        return s_len;
    //構造兩個工作向量,存放編輯距離的當前行和前一行
    int v0[t_len + 1], v1[t_len + 1];
    int i, j;
    //初始化v0,即是A[0][i],從空字符串s到目標字符串t,只要添加每個字符
    for (i = 0; i <= t_len; i++)
       v0[i] = i;
    for (i = 0; i < s_len; i++) {
        //從前一行v0計算v1,v1的第一個元素是A[i+1][0],
        //編輯距離就是從原字符串s中刪除每個字符到目標字符串t
        v1[0] = i + 1;
        for (j = 0; j < t_len; j++) {
            int cost = (s[i] == t[j]) ? 0:1;
            v1[j + 1] = Min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
        }
        if (i == s_len - 1) {
            printf("編輯距離矩陣的前一行V0是:\n");
            for (j = 0; j <= t_len; j++)
                printf("%3d", v0[j]);
            printf("\n");
        }
        //爲了下一次迭代,複製v1到v0
        for (j = 0; j <= t_len; j++)
            v0[j] = v1[j];
    }
    printf("編輯距離矩陣的當前行V1是:\n");
    for (j = 0; j <= t_len; j++)
        printf("%3d", v1[j]);
    printf("\n");
    return v1[t_len];
}
運行結果:


注:本文部分內容引用http://en.wikipedia.org/wiki/Levenshtein_distance更爲詳細的介紹可以參考英文維基百科原頁面。

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