KMP算法演绎之路

前言

模式匹配是数据结构需要解决的经典问题之一,由此衍生出许多算法。本文介绍模式匹配算法的复杂度从o(mn)逐步演化到o(m+n)的过程。

模式匹配

模式匹配描述了这样的一个问题:

字符串T[1..n]P[1..m]由字符集中的字符组成(m<=n),要求字符串T中和模式P所匹配的索引号。

朴素模式匹配算法

这个问题最简单的解决办法是:

从T的每一个字符T[i]{1≤i≤n}开始依次向后比对P的每一个字符,如果全部匹配,则输出i;重复之前的操作,直到找到所有符合条件的i

代码如下:

object KMPv1 {
  def kmp(source:String,sequence: String):scala.collection.mutable.Seq[Int] = {
    val sourceLen = source.length
    val sequenceLen = sequence.length
    if(sourceLen<sequenceLen)
      return scala.collection.mutable.Seq.empty[Int]

    var cursor = 0
    val cursorMax = sourceLen-sequenceLen+1
    var res:scala.collection.mutable.Seq[Int] = scala.collection.mutable.Seq[Int]()
    while(cursor<cursorMax){//m-n+1
      var isMatch = true
      var cursorInner = 0
      while(isMatch && cursorInner<sequenceLen){//m
        isMatch = source.charAt(cursor+cursorInner) == sequence.charAt(cursorInner)
        cursorInner+=1
      }
      if(isMatch){
        res = res.:+(cursor)
      }
      cursor+=1
    }
    res
  }
}

这个算法的复杂度是o(m*(n-m+1))=o(mn),它并没有利用到曾经匹配过的信息。

有限自动机的思想

有限自动机在接受到一个信号之后,会从一个状态转移到另外一种状态。

引入有限自动机的概念之后可以对模式匹配问题作如下变形:

将字符串T[1..n]中的字符T[i]{1≤i≤n}依次输入有限自动机,自动机的状态q记录了信号输入之后与P[1..m]相匹配的字符数,当自动机的状态转移到m时我们接受此时的索引i-m为匹配索引。

举例来说:

假设P=ababaca∑={a,b,c}中的字符组成,有限自动机的初始状态是0,在这个时候接受输入a则转移到状态1(因为输入a之后和P有一个匹配的字符);在状态为1的时候接受输入a则状态仍然是1,接受b则状态转移为2,接受c则状态转移为0(因为跟P没有匹配的字符)。

有限自动机
有限自动机

显然这个思想的关键在于构造出有限自动机的转移函数∂(q,signal)。从上图b可知,该函数只与m|∑|(全集的字符个数)有关,该表共有(m+1)|∑|个状态值,计算每个状态值的复杂度最多是o(m^2),所以构造转移函数的复杂度最多达到o(|∑|m^3)。之后与T来匹配只需要o(n)的复杂度。整个算法的复杂度最多是o(n+|∑|m^3)

此算法的复杂度远远大于朴素模式匹配算法的o(mn),但是它充分利用曾经匹配过的信息(存储在有限自动机的状态中),给了我们另外一种思路来思考这个问题,如果能将转移函数的复杂度优化到o(m),就可以大大降低整体的复杂度。

KMP算法

KMP算法在有限自动机的思想上做了一些调整,同样引入了状态这一概念,但是状态并不是由状态转移函数计算得到,而是通过前缀函数进行逻辑运算之后计算出来的。

举例来说:

如下图a,在输入T[9]=b之前,整个匹配过程处于状态q=5,即T[4..8]=P[0..4],此时输入T[9]=b,发现和P[5]=c不匹配,此时通过观察发现:

P向右移动2个位置刚好又有P[0..2]T[6..8]匹配,此时状态是q=3

输入T[9]=b之后发现匹配则状态变成q=4

尝试将上述的观察过程理论化,发现因为P[0..4]的所有前缀中(除开P[0..4]本身),与T[4..8]的后缀所匹配的最大长度是31,所以尝试将P向右移动[当前状态q=5减去3等于2]个位置;又由于有T[4..8]=P[0..4],所以1处与T[4..8]作匹配等价于与P[0..4]作匹配。

图解kmp
图解kmp

由此,下一个状态可以通过当前状态q和与P[1..q]的后缀匹配的最长前缀(除开自身)的函数来计算出。

计算前缀函数的伪代码如下:

前缀函数
前缀函数

匹配函数的伪代码如下:

匹配函数
匹配函数

scala实现如下:

/**
  * Created by studyz on 17/3/16.
  */
object KMPv2 {

  def kmpMatch(source:String, pattern: String):scala.collection.mutable.Seq[Int] = {

    var res = scala.collection.mutable.Seq[Int]()

    val statusArr = kmpPrefixFunc(pattern)

    var k =0//表示已经匹配的个数
    val n = source.length
    val m = pattern.length

    for(q <- 0 until n){//n次
      while(k>0 && source.charAt(q) != pattern.charAt(k)){
        k = statusArr(k-1)
      }
      if(source.charAt(q) == pattern.charAt(k)){
        k+=1
      }
      if(k == m){
        res = res.:+(q-m+1)
        k = statusArr(k-1)
      }
    }
    res
  }

  //pattern字符串第k位前缀的与自身匹配的最长后缀
  //P[1..m],1≤q≤m,0≤k<q,求k使得P[1..k]是P[1..q]的最长后缀,kmpPrefixFunc(q)=k
  def kmpPrefixFunc(pattern:String):Array[Int]={
    val m = pattern.length
    val res = new Array[Int](m)
    res(0) = 0
    var k = res(0)
    for(q <- 1 until m){
      while(k>0 && pattern.charAt(k) != pattern.charAt(q)){
        k = res(k)
      }
      if(pattern.charAt(k) == pattern.charAt(q)){
        k+=1
      }
      res(q) = k
    }
    res
  }
}

使用平摊分析法可知此算法的复杂度是o(m+n),其中前缀函数kmpPrefixFunc的复杂度是o(m),匹配函数kmpMatch的复杂度是o(n)(//TODO)


参考文献:

  • 算法导论第三版
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,211评论 0 4
  • 9.3.3 快速排序   快速排序将原数组划分为两个子数组,第一个子数组中元素小于等于某个边界值,第二个子数组中的...
    RichardJieChen阅读 1,832评论 0 3
  • 本系列第三篇,承接前面的《浅谈机器学习基础》和《浅谈深度学习基础》。 自然语言处理绪论 什么是自然语言处理? 自然...
    我偏笑_NSNirvana阅读 17,513评论 2 68
  • 寒雪独立枝头 外柔香飘十里 铮铮傲骨孤寂 谁曾知己难求
    男城神才阅读 213评论 4 2
  • 1.我怎么如此幸运,上午查完房,很早就回家了,外面冷,家里也冷,身体生理期,什么活也不想干,就想钻到昨晚换好的厚被...
    史真如阅读 148评论 0 0