许多程序会大量使用字符串。对于不同的字符串,我们希望能够有办法判断其相似程度。我们定义了一套操作方法来把两个不相同的字符串变得相同,具体的操作方法为:
1.修改一个字符(如把“a”替换为“b”)。
2.增加一个字符(如把“abdd”变为“aebdd”)。
3.删除一个字符(如把“travelling”变为“traveling”)。
比如,对于“abcdefg”和“abcdef”两个字符串来说,我们认为可以通过增加/减少一个“g“的方式来达到目的。上面的两种方案,都仅需要一次操作。把这个操作所需要的次数定义为两个字符串的距离,给定任意两个字符串,你是否能写出一个算法来计算出它们的距离?
分析与解法
不难看出,两个字符串的距离肯定不超过它们的长度之和(我们可以通过删除操作把两个串都转化为空串)。虽然这个结论对结果没有帮助,但至少可以知道,任意两个字符串的距离都是有限的。
我们还是应该集中考虑如何才能把这个问题转化成规模较小的同样的问题。如果有两个串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[1,…,lenA]和B[2,…,lenB]变成相同字符串。
3.一步操作之后,再将A[2,…,lenA]和B[2,…,lenB]变成相同字符串。
这样,很快就可以完成一个递归程序。
在以上面的思想完成代码后,对程序进行了一番测试。第一次找了两个相似的字符串,长度分别为15和17。速度和结果都比较满意。这也印证了算法的正确性。第二次找了两个相似的字符串,长度分别为1500和1507。嗯,直接跳出错误,说是堆栈错误。实际上是由于递归嵌套出了问题。采用递归算法,只是理论上有效,便于理解,实际应用中会出现各种限制。如本例,嵌套约1000层的时候就超过了系统的限制。必须想一个解决之道。仔细观察,可以发现用数学性的语言描述就是
F(n,m)=G(F(n,m),F(n+1,m),F(n,m+1))
这个可以简化为递推,由于递推可以放在一个函数内,就解决了系统的递归限制。
再新代码完成之后,照例还是对代码测试了一番。还是用两个相似的字符串,长度分别为1500和1507,结果能出来,但是效率差了点。在笔者的电脑上用了6秒中左右。仅仅是比较文本,就要6秒钟,比较难以接受,而且从代码看时间复杂度和空间复杂度都是O(n2)。
必须得改进!!!
在看了代码之后,发现代码运行速度慢可能出现在两个地方。一个是mDic对象,用的是Dictionary对象,在运行中反复读取和存储可能会影响速度,如果改为用数组可能效果会好点。哪位对这个有研究的同道,望不吝赐教。一个是String对象的Chars(Index)的方法。可能在每次执行到这一步时,会先把字符串转化为字符数组再返回一个字符,或者是遍历这个字符串,返回一个字符。对于本例中,大约需要执行1500×1500次,等于反复遍历,时间就浪费了。建议一开始就转化为字符数组,等到比较时就不需要遍历或转化了。
按照这两个思路对代码进行了修改,然后测试。效果很满意,本例测试几乎就是一瞬间。
程序完成之后,经测试,结果和速度都令人满意,稍显美中不足的是就是空间复杂度还是比较高,为O(S1×S2),当S1和S2都比较大的时候,可能会占用非常多的空间。
如何解决这个问题呢?
经过对计算过程的分析,我发现作为存储的二维矩阵,在每一个循环中,其实只有一行的数据参与了计算,之前的数据行就不再参与计算了。因此,从这个出发点入手,对代码进行了微调,将二维数组改为一维数组。经测试,结果和速度与之前思索之三中的代码没有差异。但空间复杂度少了很多,为O(S1)。
现将代码赋予其后,用的是VB2005
Public Class clsDistance
Private mCharA() As Char
Private mCharB() As Char
Private mCharALen As Integer
Private mCharBLen As Integer
Public Sub New(ByVal StrA As String, ByVal StrB As String)
mCharA = StrA.ToCharArray
mCharB = StrB.ToCharArray
mCharALen = mCharA.Length
mCharBLen = mCharB.Length
End Sub
Public Function CacuDistance() As Integer
Dim i As Integer
If mCharALen = 0 Then Return mCharBLen
If mCharBLen = 0 Then Return mCharALen
Dim j As Integer = Min(mCharALen, mCharBLen) - 1
Dim tP1 As Integer, tP2 As Integer
tP1 = -1
tP2 = -1
For i = 0 To j
If mCharA(i) <> mCharB(i) Then
tP1 = i
Exit For
End If
Next
If tP1 = -1 Then Return Math.Abs(mCharALen - mCharBLen)
For i = 0 To j - tP1
If mCharA(mCharALen - i - 1) <> mCharB(mCharBLen - i - 1) Then
tP2 = i
Exit For
End If
Next
If tP2 = -1 Then Return Math.Abs(mCharALen - mCharBLen)
Dim tA(mCharALen - tP1 - tP2) As Integer
For i = 0 To tA.GetUpperBound(0)
tA(i) = i
Next
Dim tN1 As Integer, tN2 As Integer, tN3 As Integer
For i = 0 To mCharBLen - tP1 - tP2 - 1
tN1 = tA(0)
tN2 = tN1 + 1
For j = 1 To tA.GetUpperBound(0)
If mCharA(mCharALen - tP2 - j) = _ mCharB(mCharBLen - tP2 - i - 1) Then
tN3 = tN1
Else
tN3 = Min(tA(j), tN1, tN2) + 1
End If
tA(j - 1) = tN2
tN2 = tN3
tN1 = tA(j)
Next
tA(tA.GetUpperBound(0)) = tN2
Next
Return tA(tA.GetUpperBound(0))
End Function
Public Function Min(ByVal ParamArray Num() As Integer) As Integer
Dim tN As Integer, i As Integer
If Num.Length = 0 Then Return Nothing
tN = Num(0)
For i = 1 To Num.GetUpperBound(0)
If Num(i) < tN Then tN = Num(i)
Next
Return tN
End Function
End Class