我們來看一個實際應用。現代搜索技術的發展很多以提供優質、高效的服務作爲目標。比如說:baidu、google、sousou等知名全文搜索系統。當我們輸入一個錯誤的query="Jave" 的時候,返回中有大量包含正確的拼寫 "Java"的網頁。當然這裏面用到的技術絕對不會是我們今天講的怎麼簡單。但我想說的是:字符串的相似度計算也是做到這一點的方法之一。
字符串編輯距離: 是一種字符串之間相似度計算的方法。給定兩個字符串S、T,將S轉換成T所需要的刪除,插入,替換操作的數量就叫做S到T的編輯路徑。而最短的編輯路徑就叫做字符串S和T的編輯距離。
舉個例子:S=“eeba” T="abac" 我們可以按照這樣的步驟轉變:(1) 將S中的第一個e變成a;(2) 刪除S中的第二個e;(3)在S中最後添加一個c; 那麼S到T的編輯路徑就等於3。當然,這種變換並不是唯一的,但如果3是所有變換中最小值的話。那麼我們就可以說S和T的編輯距離等於3了。
動態規劃解決編輯距離
動態規劃(dynamic programming)是一種解決複雜問題最優解的策略。它的基本思路就是:將一個複雜的最優解問題分解成一系列較爲簡單的最優解問題,再將較爲簡單的的最優解問題進一步分解,直到可以一眼看出最優解爲止。
動態規劃算法是解決複雜問題最優解的重要算法。其算法的難度並不在於算法本身的遞歸難以實現,而主要是編程者對問題本身的認識是否符合動態規劃的思想。現在我們就來看看動態規劃是如何解決編輯距離的。
還是這個例子:S=“eeba” T="abac" 。我們發現當S只有一個字符e、T只有一個字符a的時候,我們馬上就能得到S和T的編輯距離edit(0,0)=1(將e替換成a)。那麼如果S中有1個字符e、T中有兩個字符ab的時候,我們是不是可以這樣分解:edit(0,1)=edit(0,0)+1(將e替換成a後,在添加一個b)。如果S中有兩個字符ee,T中有兩個字符ab的時候,我們是不是可以分解成:edit(1,1)=min(edit(0,1)+1, edit(1,0)+1, edit(0,0)+f(1,1)). 這樣我們可以得到這樣一些動態規劃公式:
如果i=0且j=0 edit(0, 0)=1
如果i=0且j>0 edit(0, j )=edit(0, j-1)+1
如果i>0且j=0 edit( i, 0 )=edit(i-1, 0)+1
如果i>0且j>0 edit(i, j)=min(edit(i-1, j)+1, edit(i,j-1)+1, edit(i-1,j-1)+f(i , j) )
小注:edit(i,j)表示S中[0.... i]的子串 si 到T中[0....j]的子串t1的編輯距離。f(i,j)表示S中第i個字符s(i)轉換到T中第j個字符s(j)所需要的操作次數,如果s(i)==s(j),則不需要任何操作f(i, j)=0; 否則,需要替換操作,f(i, j)=1 。
這就是將長字符串間的編輯距離問題一步一步轉換成短字符串間的編輯距離問題,直至只有1個字符的串間編輯距離爲1。
編輯距離的實際應用
在信息檢索領域的應用我們在文章開始的時候就提到了。另外,編輯距離在自然語言文本處理領域(NLP)中是計算字符串相似度的重要方法。一般而言,對於中文語句的相似度處理,我們很多時候都是將詞作爲一個基本操作單位,而不是字(字符)。
字符串編輯距離源代碼
- package net.hr.algorithm.stroper;
- /**
- * 字符串編輯距離
- *
- * 這是一種字符串之間相似度計算的方法。
- * 給定字符串S、T,將S轉換T所需要的插入、刪除、替代操作的數量叫做S到T的編輯路徑。
- * 其中最短的路徑叫做編輯距離。
- *
- * 這裏使用了一種動態規劃的思想求編輯距離。
- *
- * @author heartraid
- *
- */
- public class StrEditDistance {
- /**字符串X*/
- private String strX="";
- /**字符串Y*/
- private String strY="";
- /**字符串X的字符數組*/
- private char[] charArrayX=null;
- /**字符串Y的字符數組*/
- private char[] charArrayY=null;
- public StrEditDistance(String sa,String sb){
- this.strX=sa;
- this.strY=sb;
- }
- /**
- * 得到編輯距離
- * @return 編輯距離
- */
- public int getDistance(){
- charArrayX=strX.toCharArray();
- charArrayY=strY.toCharArray();
- return editDistance(charArrayX.length-1,charArrayY.length-1);
- }
- /**
- * 動態規劃解決編輯距離
- *
- * editDistance(i,j)表示字符串X中[0.... i]的子串 Xi 到字符串Y中[0....j]的子串Y1的編輯距離。
- *
- * @param i 字符串X第i個字符
- * @param j 字符串Y第j個字符
- * @return 字符串X(0...i)與字符串Y(0...j)的編輯距離
- */
- private int editDistance(int i,int j){
- if(i==0&&j==0){
- //System.out.println("edit["+i+","+j+"]="+isModify(i,j));
- return isModify(i,j);
- }
- else if(i==0||j==0){
- if(j>0){
- //System.out.println("edit["+i+","+j+"]=edit["+i+","+(j-1)+"]+1");
- if(isModify(i,j) == 0) return j;
- return editDistance(i, j-1) + 1;
- }
- else{
- //System.out.println("edit["+i+","+j+"]=edit["+(i-1)+","+j+"]+1");
- if(isModify(i,j) == 0) return i;
- return editDistance(i-1,j)+1;
- }
- }
- else {
- //System.out.println("edit["+i+","+j+"]=min( edit["+(i-1)+","+j+"]+1,edit["+i+","+(j-1)+"]+1,edit["+(i-1)+","+(j-1)+"]+isModify("+i+","+j+")");
- int ccc=minDistance(editDistance(i-1,j)+1,editDistance(i,j-1)+1,editDistance(i-1,j-1)+isModify(i,j));
- return ccc;
- }
- }
- /**
- * 求最小值
- * @param disa 編輯距離a
- * @param disb 編輯距離b
- * @param disc 編輯距離c
- */
- private int minDistance(int disa,int disb,int disc){
- int dismin=Integer.MAX_VALUE;
- if(dismin>disa) dismin=disa;
- if(dismin>disb) dismin=disb;
- if(dismin>disc) dismin=disc;
- return dismin;
- }
- /**
- * 單字符間是否替換
- *
- * isModify(i,j)表示X中第i個字符x(i)轉換到Y中第j個字符y(j)所需要的操作次數。
- * 如果x(i)==y(j),則不需要任何操作isModify(i, j)=0; 否則,需要替換操作,isModify(i, j)=1。
- * @param i 字符串X第i個字符
- * @param j 字符串Y第j個字符
- * @return 需要替換,返回1;否則,返回0
- */
- private int isModify(int i,int j){
- if(charArrayX[i]==charArrayY[j])
- return 0;
- else return 1;
- }
- /**
- * 測試
- * @param args
- */
- public static void main(String[] args) {
- System.out.println("編輯距離是:"+new StrEditDistance("eeba","abac").getDistance());
- }
- }
-