KMP字符串匹配算法的实现

KMP字符串匹配算法的实现

暴力查找

  • 这是最简单的一种字符串匹配算法:
  1. 使用一个指针 i 跟踪目标文本 txt, 使用指针 j 跟踪模式字符串 pat, 将 j 置为0且不断增大,直到找到一个不匹配的字符或是模式字符串结束为止。
  2. 如果没有找到匹配,那么将指针 i 向后移动一位,继续进行匹配操作
  • 这种算法的时间复杂度是O(M*N)
def brute_force(txt, pat):
    for i in range(len(txt) - len(pat) + 1):
        for j in range(len(pat)):
            if txt[i+j] != pat[j]:
                break
            if j == len(pat) - 1:  # 找到匹配的位置
                return i
    return -1  # 未找到匹配的位置

暴力查找算法最坏的情况需要查找 M*N 次,即对于每一个目标文本中的字符,都会匹配到模式字符串中的最后一个字符

KMP字符串匹配算法

  • 在暴力查找算法中,当出现不匹配的情况时,会回退目标文本的指针以进行下一次匹配,但是在这种情况下,我们已经知道了一部分文本的内容(在匹配失败之前这些文本已经和模式字符串匹配成功),我们可以利用这个信息来避免文本指针 i的回退,从而减少时间复杂度

  • 例子

    暴力算法
    pattern  B A A A A A A A A
    
                               i
                               |   
    txt      A  B  A  A  A  A  B  A  A  A  A  A  A
                B  A  A  A  A  A 
                   B  A  A  A  A  A 
                      B  A  A  A  A  A 
                         B  A  A  A  A  A  
                            B  A  A  A  A  A 
                               B  A  A  A  A  A   --> 匹配成功
    

    在第5个字符(以0为起始位)匹配失败之后,暴力算法会将i回退到2,再次进行匹配。
    但是我们可以发现这里其实并不需要回退,因为 i 前面4位的字符都是 A ,均不能与 B 匹配,另外,文本字符串 txti 位的字符 B 与模式字符串 pat 的首位 B。
    因此,我们可以直接将 i 后移一位,以比较文本字符串中的下一个字符和模式字符串中的第2个字符:

                               i->i+1
                               |  |
    txt      A  B  A  A  A  A  B  A  A  A  A  A  A
                B  A  A  A  A  A 
                               B  A  A  A  A  A   --> 匹配成功
    
  • KMP 算法的主要思想就是提前判断如何重新开始匹配,而这种判断只取决于模式字符串本身,与目标文本没有关系

  • 字符串的前缀、后缀

    • 对于字符串 A 和 B ,如果存在 A = BS, 其中S是一个任意的非空字符串,那么我们就称 B 是 A 的前缀
    • 对于字符串 A 和 B ,如果存在 A = SB,其中S是一个任意的非空字符串,那么我们就称 B 是 A 的后缀
    • 例子:
      • 字符串 ABABCB
      • 前缀: {'A', 'AB', 'ABA', 'ABAB', 'ABABC'}
      • 后缀: {'B', 'CB', 'BCB', 'ABCB", 'BABCB'}
  • KMP算法是通过什么样的规则来判断如何重新匹配的?

    • 核心:部分匹配表(Partial Match Table)数组

      • 对于一个字符串 'ABABABCA', 它的PMT表如下

        char A B A B A B C A
        index 0 1 2 3 4 5 6 7
        value 0 0 1 2 3 4 0 1
      • PMT数组中值的定义:

        • 字符串的前缀集合与后缀集合的交集中最长元素的长度。
        • 例子: 对于字符串 ABAB
          • 前缀集合: {'A', 'AB', 'ABA'}
          • 后缀集合: {'B', 'AB', 'BAB'}
        • 前缀集合与后缀集合的交集是 'AB', 长度是2,因此PMT数组中的值是2
    • KMP算法是如何通过PMT表来进行重新匹配的?

      • 例子: 在文本字符串 'ABCAABABABABCABA' 中查找模式字符串 'ABABABCA'

                              i=6
                              |
            A  B  A  B  A  B  A  B  A  B  C  A  B  A
            A  B  A  B  A  B  C  A
                              |
                              j=6
        

        i 处失配,意味着 文本字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 匹配, 在这里是 ABABAB。
        这个字符串的前缀集合是{'A', 'AB', 'ABA', 'ABAB', 'ABABA'}, 后缀集合是{'B', 'AB','BAB', 'ABAB', 'BABAB'}, 它们的交集是'ABAB'。
        这就说明文本字符串中, i 所在位置之前的4位和模式字符串的0到4位是相同的,因此我们可以省略掉这些字符串的比较----保持 i = 6 不动, 将 j 指向模式字符串的PMT[j-1] (此处为4)位即可

                              i=6
                              |
            A  B  A  B  A  B  A  B  A  B  C  A  B  A
                  A  B  A  B  A  B  C  A
                              |
                              j=4
        
      • 代码实现:
        在第 j 位失配时,我们实际上是取第 j-1位的PMT值,为了方便,我们可以将PMT数组向后偏移一位,得到一个数组,称为next数组:

        char A B A B A B C A
        index 0 1 2 3 4 5 6 7
        value 0 0 1 2 3 4 0 1
        next -1 0 0 1 2 3 4 0
        def kmp(txt, pat):
            i = j = 0
        
            while i < len(txt) and j < len(pat):
                if j == -1 or txt[i] == pat[j]:  # 首位失配或者匹配成功,i、j都向后移动一位
                    i, j = i + 1, j + 1
                else:
                    j = next[j]
        
            if j == len(pat):
                return i - j
            else:
            return -1
        
      • next 数组的生成:

        • 求next数组的过程可以看成字符串匹配的过程:
          • 从模式字符串的第一位(不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。
                                           pmt[0] = 0 => next[1] = 0
                                           
             i
             |
          A  B  A  B  A  B  C  A           
             A  B  A  B  A  B  C  A        pmt[1] = 0 => next[2] = 0
             |
             j
          
          
                i
                |
          A  B  A  B  A  B  C  A
                A  B  A  B  A  B  C  A     pmt[2] = 1 => next[3] = 1
                |
                j
          
                   i
                   |
          A  B  A  B  A  B  C  A
                A  B  A  B  A  B  C  A     pmt[3] = 2 => next[4] = 2
                   |
                   j
          
                      i
                      |
          A  B  A  B  A  B  C  A
                A  B  A  B  A  B  C  A     pmt[4] = 3 => next[5] = 3
                      |
                      j
          
                         i
                         |
          A  B  A  B  A  B  C  A
                A  B  A  B  A  B  C  A     pmt[5] = 4 => next[6] = 4
                         |
                         j
          
                            i
                            |
          A  B  A  B  A  B  C  A
                A  B  A  B  A  B  C  A     pmt[6] = 0 => next[7] = 0
                            |
                            j
          
          
        • 代码实现:
          def get_next(pat)
              next = [-1]
              i, j = 0, -1
              while i < len(pat):
                  if j == -1 or pat[i] == pat[j]:
                      i, j = i+1, j+1
                      next.append(j)
                  else:
                      j = next[j]
              return next
          

你可能感兴趣的:(KMP字符串匹配算法的实现)