字符串匹配

1、暴力匹配

思想:逐一匹配
时间复杂度字符串匹配常用算法 - 图1
实现

  1. # -----------------------------------------
  2. # https://www.geeksforgeeks.org/naive-algorithm-for-pattern-searching/?ref=lbp
  3. def find_brute_force(pat, txt):
  4. """在txt中找到pat的匹配项,并返回txt匹配时第一个索引"""
  5. m, n = len(pat), len(txt)
  6. for begin in range(n - m + 1):
  7. match = 0
  8. while match < m:
  9. if txt[begin + match] != pat[match]:
  10. break
  11. match += 1
  12. if match == m:
  13. print("Pattern found at index ", begin)
  14. txt = "AABAACAADAABAAABAA"
  15. pat = "AABA"
  16. find_brute_force(pat, txt)

优化匹配:
思想:当部分匹配时,则跳过部分匹配项

  1. # -----------------------------------------
  2. # https://www.geeksforgeeks.org/optimized-naive-algorithm-for-pattern-searching/
  3. def opt_brute_force(pat, txt):
  4. m, n = len(pat), len(txt)
  5. begin = 0
  6. while begin <= n - m:
  7. for match in range(m):
  8. if txt[begin + match] != pat[match]:
  9. break
  10. match += 1
  11. if match == m:
  12. print("Pattern found at index ", begin)
  13. begin += m
  14. elif match == 0:
  15. begin += 1
  16. else:
  17. begin += match
  18. opt_brute_force(pat, txt)

2、KMP算法

为了解决匹配字符串中含重复字符。
思想:当检测到不匹配时(在一些匹配之后),已经知道下一个窗口文本中的一些字符。利用这个信息来避免匹配无论如何都会匹配的字符。
时间复杂度:KMP匹配算法利用模式的退化特性(模式中相同的子模式出现多次),其将最坏情况复杂度提高到O(n)
实现:

  1. # -----------------------------------------
  2. # 参考:https://www.geeksforgeeks.org/kmp-algorithm-for-pattern-searching/?ref=lbp
  3. def kmp(pat, txt):
  4. """KMP算法对pat[]进行预处理,构造一个大小为m(与模式大小相同)的辅助lps[],
  5. 用于匹配时跳过字符。Name LPS表示最长的合适前缀,也是后缀。
  6. 正确的前缀是不允许带有整个字符串的前缀。"""
  7. m, n = len(pat), len(txt)
  8. # create lps[] that will hold the longest prefix suffix values for pattern
  9. lps = [0] * m # lps[] that tells us the count of characters to be skipped.
  10. match = 0 # index for pat[]
  11. # Preprocess the pattern (calculate lps[] array)
  12. computeLPS(pat, m, lps)
  13. begin = 0 # index for txt[]
  14. while begin < n:
  15. if pat[match] == txt[begin]:
  16. begin += 1
  17. match += 1
  18. if match == m:
  19. print("Pattern found at index ", begin - match)
  20. match = lps[match - 1]
  21. # mismatch after j matches
  22. elif begin < n and pat[match] != txt[begin]:
  23. # Do not match lps[0..lps[j-1]] characters,
  24. # they will match anyway
  25. if match != 0:
  26. match = lps[match - 1]
  27. else:
  28. begin += 1
  29. def computeLPS(pat, m, lps):
  30. """Lps记录pat中每个字符在前面匹配的字符的位置;
  31. Lps [i]也可以定义为最长前缀,同时也是合适的后缀。
  32. 需要正确地使用,以确保不考虑整个子字符串。"""
  33. prev = 0 # length of the previous longest prefix suffix
  34. lps[0] = 0 # lps[0] is always 0
  35. next = 1
  36. # the loop calculates lps[begin] for begin=1 to m-1
  37. while next < m:
  38. if pat[next] == pat[prev]:
  39. prev += 1
  40. lps[next] = prev
  41. next += 1
  42. else:
  43. # This is tricky. Consider the example.
  44. # AAACAAAA and i = 7. The idea is similar
  45. # to search step.
  46. if prev != 0:
  47. prev = lps[prev - 1]
  48. # Also, note that we do not increment begin here
  49. else:
  50. lps[next] = 0
  51. next += 1
  52. txt = "ABABDABACDABABCABAB"
  53. pat = "ABABCABAB"
  54. kmp(pat, txt)

3、RK算法

思想:不直接逐位对比模式串pat和text是否相等,而是利用哈希算法,计算模式串和子串的哈希值,如果哈希值不相等,那么很明显字符串也不相等,如果相等,由于哈希算法可能会存在哈希冲突的可能,因此需要再使用朴素/暴力算法判断其是否真正相等。
时间复杂度: Rabin Karp算法的核心是,将哈希函数使用滚动哈希来计算,这样计算哈希的复杂度是O(1),整体的复杂度就变成了O(m)了。
哈希:不直接对比字符串的每个字符,而是比较其所代表的数字是否相等即可。
对于一个只有数字的字符串字符串匹配常用算法 - 图2要转换成十进制的数字,公式如下:
字符串匹配常用算法 - 图3
在此基础上,往外延伸,如果对于一个只有小写英文字符的字符串来说,是不是可以当成26进制,然后计算出一个字符串所代表的数字:
字符串匹配常用算法 - 图4
比如对于一个字符串”abcd”,要计算一个长度为2的子串的子串的哈希,先计算”ab”的:
字符串匹配常用算法 - 图5
再计算”bc”的:
字符串匹配常用算法 - 图6
看一下这里的规律,在计算hash2时,完全可以复用hash1的值,字符串匹配常用算法 - 图7
已知前一个子串的哈希值,计算后一个哈希值的过程,如下:
字符串匹配常用算法 - 图8
如果字符串过长,最后计算哈希可能会溢出。为了解决这个问题,在Rabin-Karp算法中,求哈希时,使用取余。

  1. # https://www.geeksforgeeks.org/rabin-karp-algorithm-for-pattern-searching/?ref=lbp
  2. # d is the number of characters in the input alphabet
  3. d = 256
  4. # pat -> pattern
  5. # txt -> text
  6. # q -> A prime number
  7. def search(pat, txt, q):
  8. M = len(pat)
  9. N = len(txt)
  10. i = 0
  11. j = 0
  12. p = 0 # hash value for pattern
  13. t = 0 # hash value for txt
  14. h = 1
  15. # The value of h would be "pow(d, M-1)%q"
  16. for i in xrange(M-1):
  17. h = (h*d)%q
  18. # Calculate the hash value of pattern and first window
  19. # of text
  20. for i in xrange(M):
  21. p = (d*p + ord(pat[i]))%q
  22. t = (d*t + ord(txt[i]))%q
  23. # Slide the pattern over text one by one
  24. for i in xrange(N-M+1):
  25. # Check the hash values of current window of text and
  26. # pattern if the hash values match then only check
  27. # for characters on by one
  28. if p==t:
  29. # Check for characters one by one
  30. for j in xrange(M):
  31. if txt[i+j] != pat[j]:
  32. break
  33. else: j+=1
  34. # if p == t and pat[0...M-1] = txt[i, i+1, ...i+M-1]
  35. if j==M:
  36. print "Pattern found at index " + str(i)
  37. # Calculate hash value for next window of text: Remove
  38. # leading digit, add trailing digit
  39. if i < N-M:
  40. t = (d*(t-ord(txt[i])*h) + ord(txt[i+M]))%q
  41. # We might get negative values of t, converting it to
  42. # positive
  43. if t < 0:
  44. t = t+q
  45. # Driver Code
  46. txt = "GEEKS FOR GEEKS"
  47. pat = "GEEK"
  48. # A prime number
  49. q = 101
  50. # Function Call
  51. search(pat,txt,q)

4、BM算法

BM算法定义了两个规则:
坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果”坏字符”不包含在模式串之中,则最右出现位置为-1。
好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。
2021-09-04-19-07-18-942536.png
Boyer–Moore 算法在预处理时,将为两种不同的启发法结果创建不同的数组,分别称为 Bad-Character-Shift(or The Occurrence Shift)和 Good-Suffix-Shift(or Matching Shift)
当进行字符匹配时,如果发现模式 P 中的字符与文本 T 中的字符不匹配时,将比较两种不同启发法所建议的移动位移长度,选择最大的一个值来对模式 P 的比较位置进行滑动。

4.1 坏字符规则

思想:

  • T中的坏字符出现在 P的左侧时,位移为 m-(j+1)i表示坏字符在T中的索引,j表示坏字符在P中的索引,m表示P的长度,n表示T的长度
  • T中的坏字符出现在 P的右侧时,位移为 m-k
  • T中的坏字符不出现在 P中时,位移为 m。也就是将模式整体移过坏字符。

字符串匹配常用算法 - 图10
时间复杂度:在最坏的情况下,坏字符启发式可能需要字符串匹配常用算法 - 图11时间。最坏的情况是文本和模式中的所有字符都相同。例如:txt[] = “ AAAAAAAAAAAAAAAAAA “pat[] = “ AAAAA “
最好的情况下可能取字符串匹配常用算法 - 图12。最好的情况发生在所有的文字和图案都不同的时候。
实现:

  1. def find_boyer_moore(T, P):
  2. n, m = len(T), len(P)
  3. if m == 0: return 0 # 当查找的字符串为空时,返回0
  4. last = {} # 记录P中每个字母最后出现的位置
  5. for k in range(m):
  6. last[P[k]] = k
  7. # 第一次比较时,将P与T对其
  8. i = m - 1 # 第一次T被比较的位置,从后往前比较
  9. k = m - 1 # 第一次P被比较的位置,从后往前比较
  10. while i < n: # 当T中被比较的位置小于T的长度时
  11. if T[i] == P[k]: # 当字符匹配时
  12. if k == 0:
  13. print("pattern occurs at shift = %d" % i) # 当匹配到第一个字符且相等时,打印T的索引
  14. if i + m < n:
  15. shift = last.get(T[i + m], -1)
  16. if shift == -1:
  17. i += 2*m-1
  18. else:
  19. i += 2*m - shift-1
  20. k = m - 1
  21. else: # 当P有多个字符时
  22. i -= 1 # 向前一位比较
  23. k -= 1 # 向前一位比较
  24. else:
  25. j = last.get(T[i], -1) # 当两者不匹配时,在P中找到T[i]最后出现的位置
  26. i += m - min(k, j + 1) # T[i]移动m-min(k,j+1)位,移动位数取决于j大于还是小于i
  27. k = m - 1 # 重新匹配
  28. T = "ABAAAABAACD"
  29. P = "ABA"
  30. find_boyer_moore(T, P)

4.2 好后缀规则

思想:

  • 如果模式串中存在已经匹配成功的好后缀,则把目标串与好后缀对齐,然后从模式串的最尾元素开始往前匹配。

字符串匹配常用算法 - 图13
字符串匹配常用算法 - 图14

  • 如果无法找到匹配好的后缀,找一个匹配的最长的前缀,让目标串与最长的前缀对齐(如果这个前缀存在的话)。模式串[m-s,m] = 模式串[0,s]

2021-09-04-19-07-20-470537.png

  • 如果完全不存在和好后缀匹配的子串,则右移整个模式串。

实现:

  1. """
  2. 好后缀的处理:
  3. 1 首先引入边界的(border)的概念。
  4. 边界是一个子字符串,它既是正确的后缀也是正确的前缀。
  5. 例如,在字符串 “ccacc” 中,“c” 是一个边界,“cc” 是一个边界,
  6. 因为它出现在字符串的两端,而 “cca” 不是一个边界。
  7. 边界数组 bpos 用于记录 P 中索引为i包含的边界的起始索引。
  8. P的长度m,当索引为 m 时,bpos[m]表示的索引为 m+1,表示空字符
  9. 移位位置由不能向左扩展的边界获得。
  10. 考虑模式P = ABBABAB,m = 7:
  11. - 从位置 i = 5开始的后缀AB的最宽边界从位置 7 开始的,因此 bpos[5] = 7。
  12. - 在位置 i = 2 时,后缀是 BABAB。这个后缀的最宽边界是从位置4开始的BAB,
  13. 因此 j = bpos[2] = 4。可以通过下面的例子来理解 bpos[i] = j
  14. 2 对于后缀的最长字串处理
  15. 对于每个后缀,确定该后缀所包含的整个模式的最宽边界。
  16. 在下面的预处理算法中,这个值 bpos[0]最初存储在数组移位的所有空闲项中。
  17. 但当模式的后缀比 bpos[0]短时,算法继续使用下一个更宽的模式边界,即 bpos[j]。
  18. """
  19. def good_suffix(shift, bpos, pat, m):
  20. """模式串中存在已经匹配成功的好后缀"""
  21. # m is the length of pattern
  22. i = m
  23. j = m + 1
  24. bpos[i] = j
  25. while i > 0:
  26. """if character at position i-1 is not equivalent to character at j-1,
  27. then continue searching to right of the pattern for border"""
  28. while j <= m and pat[i - 1] != pat[j - 1]:
  29. """the character preceding the occurrence of t in pattern P is different
  30. than the mismatching character in P,we stop skipping the occurrences
  31. and shift the pattern from i to j"""
  32. if shift[j] == 0: shift[j] = j - i
  33. # update the position of next border
  34. j = bpos[j]
  35. """p[i-1] matched with p[j-1], border is found.
  36. store the beginning position of border. """
  37. i -= 1
  38. j -= 1
  39. bpos[i] = j
  40. def sub_suffix(shift, bpos, pat, m):
  41. """无法找到匹配好的后缀,找一个匹配的最长的前缀"""
  42. j = bpos[0]
  43. for i in range(m + 1):
  44. ''' set the border position of the first character
  45. of the pattern to all indices in array shift
  46. having shift[i] = 0 '''
  47. if shift[i] == 0:
  48. shift[i] = j
  49. ''' suffix becomes shorter than bpos[0],
  50. use the position of next widest border
  51. as value of j '''
  52. if i == j:
  53. j = bpos[j]
  54. '''Search for a pattern in given text using
  55. Boyer Moore algorithm with Good suffix rule '''
  56. def search(text, pat):
  57. # s is shift of the pattern with respect to text
  58. s = 0
  59. m = len(pat)
  60. n = len(text)
  61. bpos = [0] * (m + 1)
  62. # initialize all occurrence of shift to 0
  63. shift = [0] * (m + 1)
  64. # do preprocessing
  65. good_suffix(shift, bpos, pat, m)
  66. sub_suffix(shift, bpos, pat, m)
  67. while s <= n - m:
  68. j = m - 1
  69. ''' Keep reducing index j of pattern while characters of
  70. pattern and text are matching at this shift s'''
  71. while j >= 0 and pat[j] == text[s + j]:
  72. j -= 1
  73. ''' If the pattern is present at the current shift,
  74. then index j will become -1 after the above loop '''
  75. if j < 0:
  76. print("pattern occurs at shift = %d" % s)
  77. s += shift[0]
  78. else:
  79. '''pat[i] != pat[s+j] so shift the pattern
  80. shift[j+1] times '''
  81. s += shift[j + 1]
  82. text = "ABAAAABAACD"
  83. pat = "ABA"
  84. search(text, pat)

5、总结

  • 从前往后遍历,从前往后比较 BF、KMP,RK
  • 从前往后遍历,从后往前比较 BM
  • KMP解决pattern里有重复字串问题
  • BM解决pattern和文本存在相同字串问题