場景
在搜索引擎項目中,我用到了最短編輯距離算法,用於對用戶輸入的查詢進行糾錯,從而優化查詢結果。比如說,我們在輸入英文單詞的時候,由於疏忽或者記憶不准確,會有拼寫錯誤的情況。以單詞beau
tiful 為例,假設我們在搜索引擎中輸入beau
itful(我故意拼錯了),看看會發生什么。
如下圖所示,雖然我把這個單詞拼錯了,但是查詢結果提示“including results for beautiful”,也就是說,它似乎知道我的查詢輸入拼寫錯誤,並根據某種算法,給我推薦了一個與之最近似的單詞(大概率就是本應正確拼寫的單詞)。在這里,就用到了最短編輯距離算法。這個場景,也稱為模糊搜索。

最短編輯距離
什么是最短編輯距離呢?假定有兩個字符串s1和s2,允許對字符串進行以下三種操作:
1. 插入一個字符
2. 刪除一個字符
3. 替換一個字符
將字符串s1轉換成字符串s2的最少操作次數就是字符串s1和字符串s2之間的最短編輯距離。兩個字符串的最短編輯距離越短,意味着兩個字符串越相似。
例1 :s1 = "geek",s2 = "gesek"
我們只需要在s1中插入一個字符,就可以把s1轉換為s2,因此,這兩個字符串的最短編輯距離就是1
例2:s1 = "cat",s2 = "cut"
我們只需要在s1中替換一個字符,就可以把s1轉換為s2,因此,這兩個字符串的最短編輯距離就是1
例3:s1 = "sunday",s2 = "saturday"
由於第1個字符和最后3個字符是一樣的,因此,我們只要考慮“un”和"atur"即可。首先,把'n'替換成'r',再插入'a'、't',因此最短編輯距離是3
以上面例3進行說明,我們
從字符串的最后一位開始,從右向左進行比較,由於最后一位都是'y',因此,不需要任何操作,也就是說,兩者的最短編輯距離等價於"sunda"和"saturda"的最短編輯距離,即d("sunday", "saturday") = d("sunda", "saturda")。因此,如果在比較的過程中遇到了相同的字符,那么二者的最短編輯距離就等價於除了這個字符之外,剩余字符的最短編輯距離,即d(i, j) = d(i-1, j-1)。
如果比較的字符不一致,比方說,已經比較到了"sun"和"satur",根據允許的操作,我們有以下3種操作:
(1)插入:在s1末尾插入一個字符'r'(即"sunr"),由於此時末尾字符都是'r',因此就變成了比較"sun"和"satu"的編輯距離,即d("sun", "satur") = d("sun", "satu") + 1,也可以寫成d(i, j) = d(i, j-1) + 1。+1 表示當前進行了一次字符操作。
(2)刪除:刪除s1的最后一個字符,並考察s1剩下的部分與s2的距離。即d("sun", "satur") = d("su", "satur") + 1,也可以寫成d(i, j) = d(i-1, j) + 1。
(3)替換:把s1的最后一個字符替換為s2的最后一個字符,即變成了"sur",因此即d("sun", "satur") = d("su", "satu") + 1,也可以寫成d(i, j) = d(i-1, j-1) + 1。
基於上述分析,我們就可以很快寫出遞歸的代碼。如下:
static int min(int x,int y,int z) { if (x<=y && x<=z) return x; if (y<=x && y<=z) return y; else return z; } static int editDist(String str1 , String str2 , int m ,int n) { // If first string is empty, the only option is to // insert all characters of second string into first if (m == 0) return n; // If second string is empty, the only option is to // remove all characters of first string if (n == 0) return m; // If last characters of two strings are same, nothing // much to do. Ignore last characters and get count for // remaining strings. if (str1.charAt(m-1) == str2.charAt(n-1)) return editDist(str1, str2, m-1, n-1); // If last characters are not same, consider all three // operations on last character of first string, recursively // compute minimum cost for all three operations and take // minimum of three values. return 1 + min ( editDist(str1, str2, m, n-1), // Insert editDist(str1, str2, m-1, n), // Remove editDist(str1, str2, m-1, n-1) // Replace ); }
但是我們都知道遞歸會存在大量的重復計算,因此,顯然不是最優解。在這里,我們可以利用動態規划的思想來進行優化。
假設dp[i][j]表示s1[i]與s2[j]的最短編輯距離,根據之前的分析,可以寫出如下代碼:
static int editDistDP(String str1, String str2, int m, int n) { // Create a table to store results of subproblems int dp[][] = new int[m+1][n+1]; // Fill d[][] in bottom up manner for (int i=0; i<=m; i++) { for (int j=0; j<=n; j++) { // If first string is empty, only option is to // insert all characters of second string if (i==0) dp[i][j] = j; // Min. operations = j // If second string is empty, only option is to // remove all characters of second string else if (j==0) dp[i][j] = i; // Min. operations = i // If last characters are same, ignore last char // and recur for remaining string else if (str1.charAt(i-1) == str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1]; // If the last character is different, consider all // possibilities and find the minimum else dp[i][j] = 1 + min(dp[i][j-1], // Insert dp[i-1][j], // Remove dp[i-1][j-1]); // Replace } } return dp[m][n]; }
時間復雜度:O(m*n)
空間復雜度:O(m*n)
參考: