算法:字符串相似度

时间:2012-09-24 20:56:11

标签: ruby algorithm string-algorithm

我正试图在InterviewStreet上解决这个挑战:https://www.interviewstreet.com/challenges/dashboard/#problem/4edb8abd7cacd

我已经有了一个有效的算法,但我会提高它的性能。你有什么建议吗?

# Enter your code here. Read input from STDIN. Print output to STDOUT
N = gets.to_i
words = []

while words.length < N do
  words << gets.sub(/\\n$/, '').strip
end 

words.each do |word|
  count = 0
  (word.length).times do |i|
    sub = word[i..-1]
    j=0
    while j < sub.length && sub[j] == word[j] do
      count += 1 
      j+=1
    end
  end
  puts count
end

谢谢, 格雷格

1 个答案:

答案 0 :(得分:3)

您的算法处于最差情况下是二次的。对于大多数普通单词,没有二次行为,并且它运行得很好(由于它的简单性,它运行速度可能比具有更好的最坏情况行为的更复杂的算法更快)。

一种具有线性最坏情况行为的算法是Z算法。我不会说太多ruby,所以目前Python版本必须这样做:

def zarray(str):
    Z = [0]*len(str)
    Z[0] = len(str)
    left, right, i = 0, 0, 1
    while i < len(str):
        if i > right:
            j, k = 0, i
            while k < len(str) and str[j] == str[k]:
                j += 1
                k += 1
            Z[i] = j
            if j > 0:
                left, right = i, i+j-1
        else:
            z = Z[i-left]
            s = right-i+1
            if z < s:
                Z[i] = z
            else:
                j, k = s, s+i
                while k < len(str) and str[j] == str[k]:
                    j += 1
                    k += 1
                Z[i] = j
                left, right = i, i+j-1
        i += 1
    return Z

def similarity(s):
    return sum(zarray(s))

算法说明:

这个想法很简单(但是,像大多数好主意一样,不容易)。让我们调用一个(非空)子字符串,它也是字符串前缀substring的前缀。为避免重新计算,该算法使用prefix-substring的窗口,该窗口在当前被考虑的索引之前开始,该索引向右延伸最远(最初,窗口为空)。

使用的变量和算法的不变量:

  • i,正在考虑的索引从1开始(对于从0开始的索引;不考虑整个字符串)并且递增到length - 1
  • leftright,prefix-substring窗口的第一个和最后一个索引;不变量:
    1. left < ileft <= right < length(S)left > 0right < 1
    2. 如果left > 0,则S[left .. right]SS[left .. ]的最大公共前缀,
    3. 如果1 <= j < iS[j .. k]S的前缀,那么k <= right
  • 数组Z,不变量:对于1 <= k < iZ[k]包含最长公共前缀S[k .. ]S的长度。

算法:

  1. 设置i = 1left = right = 0(允许使用left <= right < 1的任何值),并为所有索引Z[j] = 0设置1 <= j < length(S)
  2. 如果i == length(S),请停止。
  3. 如果i > right,找到lS最长公共前缀的长度S[i .. ],请将其存储在Z[i]中。如果l > 0我们发现窗口比前一个更向右延伸,则设置left = iright = i+l-1,否则保持不变。增加i并转到2.
  4. 此处left < i <= right,因此子字符串S[i .. right]已知 - 因为S[left .. right]S的前缀,所以它等于S[i-left .. right-left]

    现在考虑S的最长公共前缀,其子字符串从索引i - left开始。 其长度为Z[i-left]S[k] = S[i-left + k]0 <= k < Z[i-left]S[Z[i-left]] ≠ S[i-left+Z[i-left]] Z[i-left] <= right-i。现在,如果i + Z[i-left],则S[i + Z[i-left]] = S[i-left + Z[i-left]] ≠ S[Z[i-left]] S[i + k] = S[i-left + k] = S[k] for 0 <= k < Z[i-left] 位于已知窗口内,因此

    S

    我们发现S[i .. ]Z[i-left]的最长公共前缀的长度为Z[i] = Z[i-left]。 然后设置i,增加S[i .. right],然后转到2.

    否则,Sright+1的前缀,我们检查它的扩展距离,开始比较索引right+1 - il处的字符。设长度为Z[i] = l。设置left = iright = i + l - 1i,增加{{1}},然后转到2.

  5. 由于窗口从不向左移动,并且比较总是在窗口结束后开始,因此字符串中的每个字符最多成功一次与字符串中的较早字符进行比较,并且对于每个起始索引,都存在大多数不成功的比较,因此算法是线性的。