字符画之画字符串匹配

  1. 说明
  2. 蛮力匹配
  3. KMP匹配
    1. KMP算法
    2. 建立next表
    3. 改进版next表
  4. BM匹配
    1. BM算法
    2. 建立bc表
    3. 建立gs表
  5. 代码参考

用字符“画”字符串的相关匹配算法。

说明

以S代表字符串,下面是相关术语说明。

  相等:
       S[0,n) = T[0,m)
       长度相等,且对应的字符相同
  子串:
       substr(i,k) = S[i, i+k), 0<= i < n, 0 <= k
       从S[i]起的连续k个字符
  前缀:
       prefix(k) = substr(0,k) = S[0,k), 0 <= k <= n
       最靠前k个字符
  后缀:
       suffix(k) = substr(n-k, k) = S[n-k, n), 0 <= k <= n
       最靠后的k个字符
  空串:
       S[0, n=0)

蛮力匹配

蛮力匹配即是依次比较两个字符串的每一个字符,效率低下。

  • 方式1:
  时间复杂度:
  len(P) = m , len(T) = n
  O(n+m) <= BF <= O(n*m)

  示意图:
  P: Pattern,模式字符,即待匹配的字符
  T: Text,文本字符
                             i
  Text: ------ [           ] X -------------
  Pattern:     [           ] Y [     ]
                             j
  每次匹配失败:T回退 j 个字符,重新从P[0]开始比较
  若匹配成功  :则i-j为P在T首次出现位置(第一个字符)的下标
  • 方式2:
  时间复杂度:
  len(P) = m , len(T) = n
  O(n+m) <= BF <= O(n*m)

  示意图:
  P: Pattern,模式字符,即待匹配的字符
  T: Text,文本字符
                i           i+j
  Text: ------ [           ] X -------------
  Pattern:     [           ] Y [     ]
                             j

  每次匹配失败:重新从P[0]开始比较
  若匹配成功  :则i为P在T首次出现位置(第一个字符)的下标

KMP匹配

KMP利用P中匹配成功的前缀信息,来确定新的匹配起始点;
匹配概率越高,优势越明显。
适合小字符集的匹配(如数字字符集,英文字母字符集);

KMP算法

  (1)KMP的基本思想如下:
                              失配点
                            /
  Text    :  c h i n c h i *
             0 1 2 3 4 5 6 7 8 9  -> 字符的下标j
  Pattern :  c h i n c h i l l a
            -1 0 0 0 0 1 2 3 0 0  -> next[j]
  next[j] :          c h i n c h i l l a

  在 P[j=7]=l 匹配失败时,不必从P[j=0]重新开始匹配,而是从 next[j=7]=P[j=3]=n 开始匹配;
  因为 P[j=4,5,6]=chi 和 P[j=0,1,2] 相同的,不需要再次匹配(因此KMP算法需要先建立next表);

  (2)时间复杂度:
  len(P) = m , len(T) = n
  KMP = O(n+m)

建立next表

  Next[j]的建立:
                                         i -> 失配点下标
  Text: ---------- {     }{  P[j-t,j)  } x ----------
                    
                                         j -> 失配点下标
  Pattern:         {   P[0,t)   }{     } y -----
                                        
                                         t -> 新的比较点
                      j-t  {   P[0,t)  } z -------

       当P[j-t,j) 与 P[0,t)相等,则无需要再比较,直接从新的比较点t开始匹配

  对任意失配点j,next[j]的值为:
  next[j] = N(P,j) = { 0 <= t < j | P[0,t) == P[j-t, j) }
  即:
       在P的前缀P[0,j)中,P[0,j)的真前缀P[0,t)和真后缀P[j-t,j)匹配的长度,就是next[j];
       若有多个匹配的真前缀和真后缀,则取长度最长的(即使得移动距离j-t最小);
       因为“大”长度的,肯定包含了“小”长度真前缀和真后缀。

  可推出:
  next[j+1] <= next[j] + 1
  当且仅当 P[j] == P[next[j]] 时取等号
  理解如下:
               j+1
  [   P[0,j+1) |   ]
              j|
  [   P[0,j)  ||   ]
  [    *******#?   ]       => P[j] = #, next[j] = 7, next[j] < j (真后缀的长度比P[0,j)的长度小)
      [*******#?       ]   => P[7] = # = P[next[j]],则有P[j] = P[next[j]]
       --------
      此段是 P[0,j) 的真前缀和真后缀,其长度,也即 * 的个数,就是next[j] = 7 (注意P[0,j)未包括j);
      而next[j+1]相对于next[j]多了个 #,即P[0,j+1)的真前缀和真后缀匹配的长度加1

改进版next表

  设P与T进行匹配,且匹配到了T[i]处

  (1)情况一:接连好几个字符相同
      j j+1
  [0  0  0  0  0  0  1]
  -1 -1                   -> next[j]
   t t+1
  若P[j] = P[t] 且 P[j+1] = P[t+1],则next[j+1] = next[t+1] = -1;

  同理一直推下去,可知前6个0的next均为-1;
  这样,若T[i]与P[j+1]失配,可以让P直接从头开始比较,
  P[j+1]左侧的相同的0无需再比较(比较也肯定失配)。


  (2)情况二:普通情况
               j j+1
  [a  b  c  *  a  b  #]
  -1  0  0     0  1        -> next[]
  -1  0  0    -1  0        -> next_improved[]
   t t+1

  若P[j] = P[t] 且 P[j+1] = P[t+1],则next[j+1] = next[t+1] = 0;

  按原来算法,若T[i]与P[j+1]失配,
  接着,应该让T[i]与 P[next[j+1]] = P[t+1] 比较

  按照改进算法,若T[i]与P[j+1]失配,
  接着,直接让T[i]与 P[next[j+1] = P[0] 比较,
  因为既然T[i] != P[j+1],也就有T[i] != P[t+1],再比较T[i]与P[t+1]肯定也是失配
  可以这样来理解:j+1指向处于P中的自相似的子串"ab",第一个子串失配了,另一个子串也必然失配。

BM匹配

BM算法利用P匹配失败的信息,来确定新的匹配起始点;
首次失配的概率越大,性能优势越明显;
适合大字符表(如中文字符集)。

BM算法

  时间复杂度:
  len(P) = m , len(T) = n
  O(n/m) <= BC <= O(n*m)
  O(n/m) <= BC+GS <= O(n+m)

  从右向左,对字符串进行比对。
                i           i+j
  Text: ------ [           ] X [*****]----------
  Pattern:     [           ] Y [*****]
                             j: 失配点
  bc表:
       失配之处为“教训”,要想匹配成功,下一次比对的Pattern字符应当为Text中失配字符。

  gs表:
       失配之处为“经验”,与KMP类似,借助模式字符串的自相似性。

建立bc表

  bc表:bad-character

                i           i+j
  Text: ------ [           ] X [*****]----------
               |           |
               |           | j: 失配点
  Pattern:     [           ] Y [*****]
               |           |
               |     [     ] X [           ]
                             t = bc['X']
               P在P[j]处失配,若要在T[i+j]处成功匹配,则P中应该也有一个字符X;
               故可以直接右移 j-bc['X'] 重新进行比较(bc['X']表示字符X在P中的下标)。
  特殊情况1:P中有多个字符X时,取下标最大的字符X(即P中在t右侧的子串,不能再有字符X)
  特殊情况2:P中没有字符X时,取bc['X']=-1(类似的,-1为假想的通配符位置,同时,P中在t右侧的子串中,必定没有字符X)
  特殊情况3:bc['X'] > j 时(即X在Y的右侧),则只需向右移1个字符,不需要向左移;
             因为在 i 左侧的位置,都已经排除了,不可能发生成功的匹配;
             且若有多个字符X时,在j左侧也可能有X(但下标不是最大的),故只能向右移一个字符。

  总结:
       情况1,2:右移j-bc['X'], 则i = i+j-bc['X'],情况2即为i = i+j+1
       情况3:  右移1

建立gs表

  gs表:good suffix

  在不确定P中失配点j左侧有没有字符X时,才用gs表(即bc表的情况三)
                i           i+j
  Text: ------ [           ] X [abcde]----------
               |           |
               |           | j: 失配点
  Pattern:     [           ] Y [abcde]
               |           |     \
               |           |      good suffix为P的子串(若有多个good suffix,取使gs[j]最小的,即k最大的)
               |           | k   /
  Pattern:     |     [     ] # [abcde      ]
                            gs[j] = j-k && P[gs[j]]!= 'Y'

                                    h (good suffix为P的前缀)
  Pattern:                       [cde                  ]
                                    gs[j] = len(P) - h
  P在P[j]处失配,则i直接向右移gs[j]距离。


  (1) gs为子串和前缀时的区别:
  Text:    -----------------X--------------
                        |<-- r1 -->|    => 子串与后缀匹配
                  i     |   j      |
  Pattern: [abc   #   abc X * X  abc]
              |           k        |
              |<------- r2 ------->|    => 前缀与后缀匹配
           在 P[j] = * 处失配时,向右移r1;当然,会不会漏掉 P[k] = X 的对齐位置呢?不会,因为若要在 P[k] 处成功匹配
                                          所有字符串,则 P[k] 与 P[j] 之间必定含有子串 abc,那么 r1 距离的起点就应
                                          该在 P[k] 与 P[j] 之间
           在 P[i] = # 处失配时,向右移r2;


  (2) 由ss计算gs:
      j,ss[j] = 4
  [abcd        abcd]
  ss[j]:即在P中与P后缀相同,且以P[j]为未字符的子串(或前缀)的长度

  (2.1)若存在为前缀的good suffix,必有 ss[j]=j+1, ss[i]=i+1:
   0 i  j        k  h
  [abcabc        abcabc]
                [abcabc        abcabc]
                   [abcabc        abcabc]
  在[0,k)之间发生失配时,均可以右移gs[x] = Len(P)-ss[j] = Len(P)-(j+1)
  在[k,h)之间发生失配时,均可以右移gs[x] = Len(P)-ss[i] = Len(P)-(i+1)
  其中有:k = Len(P) - (j+1)
          h = Len(P) - (i+1)

  (2.2)若存在为子中的good suffix,必有ss[j] < j+1:
   0     j         i: 失配点
  [    abc          abc]
              [abcd        abcd]
  在i失配,则右移gs[i] = Len(P) - (j+1), i = Len(P) - ss[j] - 1
  ss[j] = 0       : i = Len(P) - ss[j] - 1 = max-index
  0 < ss[j] < j+1 : i = Len(P) - ss[j] - 1


  (3) 示例:
   0  1  2  3  4  5  6  7  => index
   非 曰 静 也 善 故 静 也
   A  B  C  D  E  F  C  D
   0  0  0  2  0  0  0  8  => ss
   8  8  8  8  8  4  8  1  => gs

   A  B  A  B  x  y  z  A  B  A  B
   0  2  0  4  0  0  0  0  2  0  11 => ss
   7  7  7  7  7  7  7  9  2 11  1  => gs

代码参考


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [ yehuohan@gmail.com ]

文章标题:字符画之画字符串匹配

本文作者:Y

发布时间:2018-06-29, 23:02:02

最后更新:2019-05-21, 20:44:46

原始链接:http://yehuohan.github.io/2018/06/29/%E7%AC%94%E8%AE%B0/DSA/%E5%AD%97%E7%AC%A6%E7%94%BB%E4%B9%8B%E7%94%BB%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。