1. 程式人生 > >計算字串的相似度-兩種解法

計算字串的相似度-兩種解法

一直不理解,為什麼要計算兩個字串的相似度呢。什麼叫做兩個字串的相似度。經常看別人的部落格,碰到比較牛的人,然後就翻了翻,終於找到了比較全面的答案和為什麼要計算字串相似度的解釋。因為搜尋引擎要把通過爬蟲抓取的頁面給記錄下來,那麼除了通過記錄url是否被訪問過之外,還可以這樣,比較兩個頁面的相似度,因為不同的url中可能記錄著相同的內容,這樣,就不必再次記錄到搜尋引擎的儲存空間中去了。還有,大家畢業的時候都寫過論文吧,我們論文的查重系統相信也會採用計算兩個字串相似度這個概念。

以下敘述摘自程式設計之美一書:

許多程式會大量使用字串。對於不同的字串,我們希望能夠有辦法判斷其相似程式。我們定義一套操作方法來把兩個不相同的字串變得相同,具體的操作方法為:
1.修改一個字元(如把“a”替換為“b”);  
2.增加一個字元(如把“abdd”變為“aebdd”);
3.刪除一個字元(如把“travelling”變為“traveling”);
比如,對於“abcdefg”和“abcdef”兩個字串來說,我們認為可以通過增加/減少一個“g”的方式來達到目的。上面的兩種方案,都僅需要一 次 。把這個操作所需要的次數定義為兩個字串的距離,而相似度等於“距離+1”的倒數。也就是說,“abcdefg”和“abcdef”的距離為1,相似度 為1/2=0.5。
給定任意兩個字串,你是否能寫出一個演算法來計算它們的相似度呢?
原文的分析與解法  
   不難看出,兩個字串的距離肯定不超過它們的長度之和(我們可以通過刪除操作把兩個串都轉化為空串)。雖然這個結論對結果沒有幫助,但至少可以知道,任意兩個字串的距離都是有限的。我們還是就住集中考慮如何才能把這個問題轉化成規模較小的同樣的子問題。如果有兩個串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[2,...,lenA]和B[2,...,lenB]變成相字串。
3.一步操作之後,再將A[1,...,lenA]和B[2,...,lenB]變成相字串。

通過以上1和6,2和5,3和4的結合操作,最後兩個字串每個對應的字元會相同,但是這三種操作產生的最終的兩個字串是不一樣的。我們不知道通過上述的三種結合那種使用的操作次數是最少的。所以我們要比較操作次數來求得最小值。

下面這幅圖是摘自程式設計之美:從中我們可以看出一些資訊。

                                             

可以看到,在計算的過程中,有索引越界的情況,抓住這個特點,就可以儘早的結束程式,同時還有重複計算的情況,比如(strA, 2, 2, strB, 2, 2).為了減少計算的次數,可以採用臨時陣列儲存中間的結果。下面給出程式:


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int min(int a, int b, int c) {
if(a < b) {
if(a < c)
return a;
else
return c;
} else {
if(b < c)
return b;
else
return c;
}
}
int compute_distance(char *strA, int pABegin, int pAEnd, char *strB, int pBBegin, int pBEnd, int **temp) {
int a, b, c;
if(pABegin > pAEnd) {
if(pBBegin > pBEnd) {
return 0;
} else {
return pBEnd - pBBegin + 1;
}
}

if(pBBegin > pBEnd) {
if(pABegin > pAEnd) {
return 0;
} else {
return pAEnd - pABegin + 1;
}
}

if(strA[pABegin] == strB[pBBegin]) {
if(temp[pABegin + 1][pBBegin + 1] != 0) {
a = temp[pABegin + 1][pBBegin + 1];
} else {
a = compute_distance(strA, pABegin + 1, pAEnd, strB, pBBegin + 1, pBEnd, temp);
}
return a;
} else {
if(temp[pABegin + 1][pBBegin + 1] != 0) {
a = temp[pABegin + 1][pBBegin + 1];
} else {
a = compute_distance(strA, pABegin + 1, pAEnd, strB, pBBegin + 1, pBEnd, temp);
temp[pABegin + 1][pBBegin + 1] = a;
}

if(temp[pABegin + 1][pBBegin] != 0) {
b = temp[pABegin + 1][pBBegin];
} else {
b = compute_distance(strA, pABegin + 1, pAEnd, strB, pBBegin, pBEnd, temp);
temp[pABegin + 1][pBBegin] = b;
}

if(temp[pABegin][pBBegin + 1] != 0) {
c = temp[pABegin][pBBegin + 1];
} else {
c = compute_distance(strA, pABegin, pAEnd, strB, pBBegin + 1, pBEnd, temp);
temp[pABegin][pBBegin + 1] = c;
}

return min(a, b, c) + 1;
}

}

void main() {
char a[] = "efsdfdabcdefgaabcdefgaabcdefgaabcdefgasfabcdefgefsdfdabcdefgaabcdefgaabcdefgaabcdefgasfabcdefg";
char b[] = "efsdfdabcdefgaabcdefgaaefsdfdabcdefgaabcdefgaabcdefgaabcdefgasfabcdabcdefggaabcdefgasfabcdefg";
int len_a = strlen(a);
int len_b = strlen(b);

int **temp = (int**)malloc(sizeof(int*) * (len_a + 1));
for(int i = 0; i < len_a + 1; i++) {
temp[i] = (int*)malloc(sizeof(int) * (len_b + 1));
memset(temp[i], 0, sizeof(int) * (len_b + 1));
}
memset(temp, 0, sizeof(temp));
int distance = compute_distance(a, 0, len_a - 1, b, 0, len_b - 1, temp);
printf("%d\n", distance);
}

之所以在return min(a, b, c) + 1,進行加1的操作,是為了表示在當前兩個對應位置的字元不相等的時候,我們採取了一次操作,不管是上述6種情況中的哪一種。而當兩個字元相等的時候,就不需要加1,因為沒有進行操作。這種方法是從前向後的操作,還有一種就是採用動態規劃的形式。如果想理解動態規劃的這種方式,建議先學習求兩個字串的最常公共子序列。所謂最常公共子序列就是給定兩個字串s1,s2,找出一個最常的字串s3,s3中的每個字元同時在s1,s2中出現,但是不必連續。下面給出應用動態規劃解決字串相似度的解釋和程式原始碼.
設Ai為字串A(a1a2a3 … am)的前i個字元(即為a1,a2,a3 … ai)
設Bj為字串B(b1b2b3 … bn)的前j個字元(即為b1,b2,b3 … bj)
設 L(i,j)為使兩個字串和Ai和Bj相等的最小操作次數。
當ai==bj時 顯然 L(i,j) = L(i-1,j-1)
當ai!=bj時 
 若將它們修改為相等,則對兩個字串至少還要操作L(i-1,j-1)次
 若刪除ai或在bj後新增ai,則對兩個字串至少還要操作L(i-1,j)次
 若刪除bj或在ai後新增bj,則對兩個字串至少還要操作L(i,j-1)次
 此時L(i,j) = min( L(i-1,j-1), L(i-1,j), L(i,j-1) ) + 1 
顯然,L(i,0)=i,L(0,j)=j, 再利用上述的遞推公式,可以直接計算出L(i,j)值。L(i,0)代表Ai和B0,如果想把一個字串和一個空字串變的相同,那麼之後刪除非空串中的字元或者把空串變成和非空串相同的字串,那麼所需要操作的次數為i次。


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int min(int a, int b, int c) {
if(a < b) {
if(a < c)
return a;
else
return c;
} else {
if(b < c)
return b;
else
return c;
}
}
int compute_distance(char *strA, int len_a, char *strB, int len_b, int **temp) {
int i, j;

for(i = 1; i <= len_a; i++) {
temp[i][0] = i;
}

for(j = 1; j <= len_b; j++) {
temp[0][j] = j;
}

temp[0][0] = 0;

for(i = 1; i <= len_a; i++) {
for(j = 1; j <= len_b; j++) {
if(strA[i -1] == strB[j - 1]) {
temp[i][j] = temp[i - 1][j - 1];
} else {
temp[i][j] = min(temp[i - 1][j], temp[i][j - 1], temp[i - 1][j - 1]) + 1;
}
}
}
return temp[len_a][len_b];
}

void main() {
char a[] = "efsdfdabcdefgaabcdefgaabcdefgaabcdefgasfabcdefgefsdfdabcdefgaabcdefgaabcdefgaabcdefgasfabcdefg";
char b[] = "efsdfdabcdefgaabcdefgaaefsdfdabcdefgaabcdefgaabcdefgaabcdefgasfabcdabcdefggaabcdefgasfabcdefg";
int len_a = strlen(a);
int len_b = strlen(b);

int **temp = (int**)malloc(sizeof(int*) * (len_a + 1));
for(int i = 0; i < len_a + 1; i++) {
temp[i] = (int*)malloc(sizeof(int) * (len_b + 1));
memset(temp[i], 0, sizeof(int) * (len_b + 1));
}
int distance = compute_distance(a, len_a, b, len_b, temp);
printf("%d\n", distance);
}
最終兩個解法的執行結果是一樣的。敬請讀者驗證。
---------------------
作者:yingsun
來源:CSDN
原文:https://blog.csdn.net/zzran/article/details/8274735
版權宣告:本文為博主原創文章,轉載請附上博文連結!