文章目錄
- 最長公共子序列
- 求解最長公共子序列
- 如何求出最長的公共子序列
- 如何實現(xiàn)文本比對
- 比對效果圖
- 參考文章:
最近因為項目需求需要實現(xiàn)一個文本比對的功能,自然的就想到了git的文本比對功能,于是網(wǎng)上查閱了一些資料,看到了一個關(guān)鍵字(最長公共子序列),感覺又回到了大學刷題的時候了。
最長公共子序列
引用LeetCode第1143題的描述
給定兩個字符串 text1 和 text2,返回這兩個字符串的最長公共子序列的長度。
一個字符串的 子序列
是指這樣一個新的字符串:它是由原字符串在不改變字符的相對順序的情況下刪除某些字符(也可以不刪除任何字符)后組成的新字符串。 例如,“ace”
是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。兩個字符串的「公共子序列」是這兩個字符串所共同擁有的子序列。
若這兩個字符串沒有公共子序列,則返回 0。
示例 1:
輸入:text1 = “abcde”, text2 = “ace” 輸出:3 解釋:最長公共子序列是 “ace”,它的長度為 3。
示例 2:
輸入:text1 = “abc”, text2 = “abc” 輸出:3 解釋:最長公共子序列是 “abc”,它的長度為 3。 示例 3:
輸入:text1 = “abc”, text2 = “def” 輸出:0 解釋:兩個字符串沒有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000 1 <= text2.length <= 1000 輸入的字符串只含有小寫英文字符。
從上面可以知道最長公共子序列是指兩個字符串里面連續(xù)不一定相鄰的最長的公共字符
求解最長公共子序列
最長公共子序列的問題可以使用dp(動態(tài)規(guī)劃)的方式來求解,但是使用動態(tài)規(guī)劃需要確定狀態(tài)轉(zhuǎn)移方程。
首先我們假定需要求解的字符串是 sda3b225cwsadbs 和 ass3bcdssd2cpsekld998l ,其中他們的公共子串是 s3b2csd
確定狀態(tài)轉(zhuǎn)移方程
我們可以使用一個比較簡單的例子來推導狀態(tài)轉(zhuǎn)移方程
比如 1a2b3cdef , asdcdrf 子串為 acdf
首先我們假設(shè)現(xiàn)在有一個方法可以計算出最長的公共子序列,我們把它設(shè)為 lcs(m,n)
那么 lcs(1,"") = 0,lcs("",a) = 0,lcs(1,a) = 0,lcs(1a,a)=1 ,lcs(1,as) =0,lcs(1a,as) =1
上面的推導里面可以得知
如果我們需要需要求出字符串 1a和as的最長公共子序列那么我們需要知道 字符串 1,as 以及 字符串1a,a 的最長公共子序列。
因此我們可以得到如下公式:
已知 lcs(x,"") =0 , lcs("",y) =0
那么 lcs(x,y) 可以求解:
if x=y
lcs(x,y) = max(lcs(x,"") , lcs("",y) ) +1
else
lcs(x,y) = max(lcs(x,"") , lcs("",y) )
根據(jù)上面的狀態(tài)轉(zhuǎn)移方程我們可以很輕易的寫出對應的代碼
public LcsResult
lcs(List
<String> source
, List
<String> target
){int[][] dp
= new int[source
.size()+1][target
.size()+1];int max
= 0;for (int i
= 1; i
< (source
.size() + 1); i
++) {for (int j
= 1; j
< (target
.size() + 1); j
++) {String str1
= source
.get(i
-1);String str2
= target
.get(j
-1);if(str1
.equals(str2
)){int temp
=dp
[i
][j
] =dp
[i
-1][j
-1]+1;max
= Math
.max(max
,temp
);}else{dp
[i
][j
] = Math
.max(dp
[i
-1][j
],dp
[i
][j
-1]);}}}return findStr(dp
,target
);}
如何求出最長的公共子序列
在上面的動態(tài)規(guī)劃代碼里面我們已經(jīng)得到了一個狀態(tài)矩陣
如下圖:
通過動態(tài)規(guī)劃矩陣找出最長的子序列,我們只需要通過回溯dp數(shù)組就可以找出子序列
我們從最后一個位置開始往前回溯,分別判斷左邊和上邊與當前位置是否相等,如果相等那么向左或者向上移動一位,如果都不相等那么這個就是我們需要的字符,記錄下來,然后同時向左和向上移動一位
但是我們在通過回溯求出最長的公共子序列的時候會遇到多解的問題,如果左邊和上邊都與當前位置相等怎么辦,這時候既可以向左也可以向上,不過得出來的結(jié)果可能會不一樣
下面這張圖的結(jié)果是采用了默認向上的做法,得出來的子序列是:acdf
代碼如下:
private LcsResult
findStr(int[][] dp
, List
<String> target
) {LcsResult lcsResult
= new LcsResult();lcsResult
.setCommonString(new ArrayList<>());lcsResult
.setSourceIndex(new ArrayList<>());lcsResult
.setTargetIndex(new ArrayList<>());int i
=dp
.length
-1;for (int j
= dp
[i
].length
-1; j
> 0 && i
>0;) {int k
= dp
[i
][j
];int up
= dp
[i
-1][j
];int left
= dp
[i
][j
-1];if(up
==k
){i
--;} else if(left
==k
) {j
--;}else{int targetIndex
= j
-1;int sourceIndex
= i
-1;lcsResult
.getSourceIndex().add(sourceIndex
);lcsResult
.getTargetIndex().add(targetIndex
);lcsResult
.getCommonString().add(target
.get(targetIndex
));i
--;j
--;}}Collections
.reverse(lcsResult
.getCommonString());Collections
.reverse(lcsResult
.getSourceIndex());Collections
.reverse(lcsResult
.getTargetIndex());return lcsResult
;}
如何實現(xiàn)文本比對
當我們求出來最長的公共子序列之后那么我們?nèi)绾螌@兩個字符串進行比對
現(xiàn)在有如下兩個字符串
str1= 1a2b3cdef
str2= asdcdrf
公共子序列為 acdf
如下圖,我們以公共子串為標準進行錯位對比,可以看到,我們能夠很容易的分辨出
在str1上面新增了一個字符串 a,同時str1上面的 2b3 被修改成了 sd ,e 被修改成了f
通過上面的對比,我們只需要把元素換成字符串就可以進行文本比對了,代碼如下
@Overridepublic List
<CompareResult> compare(List
<String> source
, List
<String> target
) {LCS lcs
= new LCS();LcsResult lcsResult
= lcs
.lcs(source
, target
);return compare(lcsResult
,source
,target
);}public List
<CompareResult> compare(LcsResult lcsResult
, List
<String> source
, List
<String> target
) {List
<CompareResult> res
= new ArrayList<>();int lastSourceIndex
= 0;int lastTargetIndex
= 0;for (int i
= 0; i
< lcsResult
.getCommonString().size(); i
++) {int sourceIndex
= lcsResult
.getSourceIndex().get(i
);int targetIndex
= lcsResult
.getTargetIndex().get(i
);List
<String> sourceTemp
= source
.subList(lastSourceIndex
,sourceIndex
);List
<String> targetTemp
= target
.subList(lastTargetIndex
,targetIndex
);compareLeftAndRight(sourceTemp
,targetTemp
,res
);lastSourceIndex
= sourceIndex
+1;lastTargetIndex
= targetIndex
+1;res
.add(new CompareResult(CompareResult
.RESULT_EQUAL
,source
.get(sourceIndex
),target
.get(targetIndex
),true));}List
<String> sourceTemp
= lastSourceIndex
>=source
.size()?new ArrayList<>():source
.subList(lastSourceIndex
,source
.size());List
<String> targetTemp
= lastTargetIndex
>=target
.size()?new ArrayList<>():target
.subList(lastTargetIndex
,target
.size());compareLeftAndRight(sourceTemp
,targetTemp
,res
);return res
;}private void compareLeftAndRight(List
<String> sourceTemp
, List
<String> targetTemp
, List
<CompareResult> res
) {if(CollectionUtils
.isEmpty(sourceTemp
)){targetTemp
.forEach(item
-> res
.add(new CompareResult(CompareResult
.RESULT_INSERT
,"",item
,true)));}else if(CollectionUtils
.isEmpty(targetTemp
)){sourceTemp
.forEach(item
-> res
.add(new CompareResult(CompareResult
.RESULT_DELETE
,item
,"",true)));}else if(targetTemp
.size()==sourceTemp
.size()){for (int k
= 0; k
< targetTemp
.size(); k
++) {res
.add(new CompareResult(CompareResult
.RESULT_CHANGE
,sourceTemp
.get(k
),targetTemp
.get(k
),true));}}else if(sourceTemp
.size()>targetTemp
.size()){for (int k
= 0; k
< sourceTemp
.size(); k
++) {res
.add(new CompareResult(k
>=targetTemp
.size()?CompareResult
.RESULT_DELETE
:CompareResult
.RESULT_CHANGE
,sourceTemp
.get(k
),k
>=targetTemp
.size()?"":targetTemp
.get(k
),false));}}else{for (int k
= 0; k
< targetTemp
.size(); k
++) {res
.add(new CompareResult(k
>=sourceTemp
.size()?CompareResult
.RESULT_INSERT
:CompareResult
.RESULT_CHANGE
,k
>=sourceTemp
.size()?"":sourceTemp
.get(k
),targetTemp
.get(k
),false));}}}
CompareResult
@Data
@AllArgsConstructor
public class CompareResult implements Serializable
{public static final String RESULT_EQUAL
= "EQUAL";public static final String RESULT_INSERT
= "INSERT";public static final String RESULT_DELETE
= "DELETE";public static final String RESULT_CHANGE
= "CHANGE";/*** 內(nèi)容行標記,刪除還是新增還是修改*/private String tag
;/*** 舊文本*/private String oldText
;/*** 新文本*/private String newText
;/*** 是否需要逐字比對樣式,如果不需要,那么就整體標記*/private boolean isNeedCheckDetail
;}
比對效果圖
當然,效果圖是Word的文本比對,其中還有很大一部分關(guān)于Word方面的代碼,就不貼出來了,核心思想就是上面的文本比對了
參考文章:
類似git/linux的文件對比功能(diff)是怎么實現(xiàn)的?
最長公共子串
總結(jié)
以上是生活随笔為你收集整理的JAVA实现基于LCS(最长公共子序列)的文本比对的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。