編輯距離(Edit Distance),又稱Levenshtein距離,是指兩個字串之間,由一個轉成另外一個所需的最少編輯操做次數。許可的編輯操做包括將一個字符替換成另外一個字符,插入一個字符,刪除一個字符。通常來講,編輯距離越小,兩個串的類似度越大。算法
舉個例子,給定 2 個字符串str_a=「yes」, str_b=「yeah」. 編輯距離是將 str_a 轉換爲 str_b 的最少操做次數,操做只容許以下 3 種:數組
c
-> abc
c
-> abd
那麼從str_a到str_b的轉換過程總共須要兩步:yes > yeas > yeah
或者 yes > yea > yeah
,因此str_a和str_b的編輯距離爲2。app
假設字符串a
, 共m
位,從a[1]
到a[m]
, 字符串b
, 共m
位, 從b[1]
到b[m]
. 用二維數組D
來保存由a
向b
的編輯距離,其中D[i][j]
表示字符串a[1]-a[i]
轉換爲b[1]-b[i]
的編輯距離.優化
遞歸的思想須要能夠將問題拆解,假設a[i]
和b[j]
分別是字符串a
和b
的最後一位,那麼要把問題拆解,有三種選擇:spa
a[i-1]
, b[j]
,即用a[1:i-1]
繼續和b[1:j]
比較,刪除了a[i]
,須要額外一步代價;a[i-1]
, b[j-1]
,即用a[1:i-1]
繼續和b[1:j-1]
比較,若是a[i]
和b[j]
相等,那麼無需額外代價,不然須要額外一步代價將a[i]
修改成b[j]
;a[i]
, b[j-1]
,即用a[1:i]
繼續和b[1:j-1]
比較,刪除了b[j]
,須要額外一步代價;換一種說法,也就是說具體要拆解爲哪種,須要考慮a[i]
和b[j]
的比值,以及這三種方法的代價。即以下遞歸規律:code
a[i]
等於b[j]
時,好比 abc
和bbc
,那麼D[i][j] = D[i-1][j-1]
, 即等於ab
和bb
的編輯距離;a[i]
不等於b[j]
時,D[i][j]
等於以下3項的最小值:
D[i-1][j] + 1
,即刪除a[i]
, 好比abcd -> abc的編輯距離 = abc -> abc 的編輯距離 + 1
D[i][j-1] + 1
,即插入b[j]
, 好比ab -> abc 的編輯距離 = abc -> abc 的編輯距離 + 1
D[i-1][j-1] + 1
,將a[i]
替換爲b[j]
, 好比abd -> abc 的編輯距離 = abc -> abc 的編輯距離 + 1
那麼遞歸邊界如何設定呢?orm
遞歸邊界就是a[1:i]
或者b[1:j]'
爲空的時候,即:遞歸
a[i][0] = i
, b
字符串爲空,那麼須要將a[1]-a[i]
所有刪除,因此編輯距離爲i
a[0][j] = j
, a
字符串爲空,那麼須要向a
插入b[1]-b[j]
,因此編輯距離爲j
資源
Python代碼:rem
def recursive_edit_distance(str_a, str_b): if len(str_a) == 0: return len(str_b) elif len(str_b) == 0: return len(str_a) elif str_a[len(str_a)-1] == str_b[len(str_b)-1]: return recursive_edit_distance(str_a[0:-1], str_b[0:-1]) else: return min([ recursive_edit_distance(str_a[:-1], str_b), recursive_edit_distance(str_a, str_b[:-1]), recursive_edit_distance(str_a[:-1], str_b[:-1]) ]) + 1 str_a = "yes" str_b = "yeah" print(recursive_edit_distance(str_a, str_b)) # output is : 2
算法分析:該算法邏輯清晰,可讀性較高,可是對於計算機而言卻很不友好,時間複雜度高,隨字符串長度呈指數級增加,並且遞歸算法的通病就是調用棧太深的時候,須要佔用較多計算機資源。
若是熟悉動態規劃的同窗,從上邊的思路能夠很容易推理出動態規劃的遞推公式:
if a[i] == b[j]: edit_distance(a[i], b[j]) = edit_distance(a[i-1], b[j-1]) if a[i] != b[j]: edit_distance(a[i], b[j]) = MIN ( edit_distance(a[i-1], b[j]) + 1, # 從a中刪除a[i] edit_distance(a[i], b[j-1]) + 1, # 向a中插入b[j] edit_distance(a[i-1], b[j-1]) + 1 # 將a[i]修改成b[j] )
轉換爲Python,也就是用二維數組D來記錄從a向b的轉換過程:
def edit_distance(str_a, str_b): if str_a == str_b: return 0 if len(str_a) == 0: return len(str_b) if len(str_b) == 0: return len(str_a) # 初始化dp矩陣 dp = [[0 for _ in range(len(str_a) + 1)] for _ in range(len(str_b) + 1)] # 當a爲空,距離和b的長度相同 for i in range(len(str_b) + 1): dp[i][0] = i # 當b爲空,距離和a和長度相同 for j in range(len(str_a) + 1): dp[0][j] = j # 遞歸計算 for i in range(1, len(str_b) + 1): for j in range(1, len(str_a) + 1): dp[i][j] = dp[i-1][j-1] if str_a[j-1] != str_b[i-1]: dp[i][j] = min([dp[i-1][j-1], dp[i-1][j], dp[i][j-1]]) + 1 return dp[len(str_b)][len(str_a)] str_a = "yes" str_b = "yeah" print(edit_distance(str_a, str_b)) # output is : 2
上邊的算法中用二維數組來存儲從a到b的距離,從遞推公式來看,其實每一步dp[i][j]的計算只依賴a[i]和b[j]是否相等
以及矩陣中的三個值
:
其實咱們能夠用一維數組來達到上述目的,具體能夠看Python代碼:
def edit_distance(str_a, str_b): if str_a == str_b: return 0 if len(str_a) == 0: return len(str_b) if len(str_b) == 0: return len(str_a) dp = [x for x in range(len(str_b) + 1)] for i in range(1, len(str_a) + 1): # 注意每次left_up和dp[0]的初始化 left_up = i - 1 dp[0] = i # 當前輪最左的left for j in range(1, len(str_b) + 1): up= dp[j] # j是上一輪的值,即up left = dp[j-1] # j-1是當前輪的值,即left if str_a[i-1] == str_b[j-1]: dp[j] = left_up else: dp[j] = min([left, up, left_up]) + 1 left_up = up # 每移動一步,上一輪的up就變成了left_up return dp[len(str_b)] str_a = "yes" str_b = "yeah" print(edit_distance(str_a, str_b)) # output is : 2
def edit_distance_Omn(str_a, str_b): if str_a == str_b: return 0 if len(str_a) == 0: return len(str_b) if len(str_b) == 0: return len(str_a) dp = [[0 for _ in range(len(str_a) + 1)] for _ in range(len(str_b) + 1)] for i in range(len(str_b) + 1): dp[i][0] = i for j in range(len(str_a) + 1): dp[0][j] = j for i in range(1, len(str_b) + 1): for j in range(1, len(str_a) + 1): dp[i][j] = dp[i-1][j-1] if str_a[j-1] != str_b[i-1]: dp[i][j] = min([dp[i-1][j-1], dp[i-1][j], dp[i][j-1]]) + 1 #打印完整路徑矩陣(這一步非必要) for i in range(len(str_b) + 1): for j in range(len(str_a) + 1): print dp[i][j], print # 準備倒着查詢編輯路徑,從右下角開始 i , j = len(str_b), len(str_a) op_list = [] # 記錄編輯操做 while i > 0 and j > 0: if dp[i][j] == dp[i-1][j-1]: op_list.append("keep [ {} ]".format(str_b[i-1])) i, j = i-1, j-1 continue if dp[i][j] == dp[i-1][j] + 1: op_list.append("remove [ {} ]".format(str_b[i-1])) i, j = i-1, j continue if dp[i][j] == dp[i-1][j-1] + 1: op_list.append("change [ {} ] to [ {} ]".format(str_b[i-1], str_a[j-1])) i, j = i-1, j-1 continue if dp[i][j] == dp[i][j-1] + 1: op_list.append("insert [ {} ]".format(str_a[j-1])) i, j = i, j-1 for i in range(len(op_list)): print op_list[len(op_list)-i-1] return dp[len(str_b)][len(str_a)] str_a = "yesxxxxxx" str_b = "yeahxxxxxhh" print(edit_distance(str_a, str_b))
輸出
0 1 2 3 4 5 6 7 8 9 1 0 1 2 3 4 5 6 7 8 2 1 0 1 2 3 4 5 6 7 3 2 1 1 2 3 4 5 6 7 4 3 2 2 2 3 4 5 6 7 5 4 3 3 2 2 3 4 5 6 6 5 4 4 3 2 2 3 4 5 7 6 5 5 4 3 2 2 3 4 8 7 6 6 5 4 3 2 2 3 9 8 7 7 6 5 4 3 2 2 10 9 8 8 7 6 5 4 3 3 11 10 9 9 8 7 6 5 4 4 keep [ y ] keep [ e ] change [ a ] to [ s ] change [ h ] to [ x ] keep [ x ] keep [ x ] keep [ x ] keep [ x ] keep [ x ] remove [ h ] remove [ h ] 4