编码:KR字符串匹配,一个简单到领导都看得懂的算法

常怀感恩,生活或许就不会到处深渊。 算法

这几天看了《柔性字符串匹配》,以为颇有意思。书是好书,只是这个脑子是否是猪脑就不知道了,因而秉着知之为知之,不知为不知的精神,我准备再次去请教一下个人领导,在一个月黑风高的夜晚,我给领导发了个消息,领导这么回复了我。 网络

图片

01函数

工具

**KR算法
**编码

话说回来,咱们今天要说的这个字符串匹配算法比以前讲过的kmp,horspool,sunday简单的多的字符串匹配算法,咱们知道暴力匹配是经过对两个字符串进行每个位置字符对比来查找匹配的上的子字符串。今天说的这个KR算法的思想和暴力匹配有些许相似,不过在实现上作了一些改进,这也是为何说这个算法很是容易理解的缘由,由于思路很是直接。spa

    在计算机科学中,Rabin–Karp算法或Karp–Rabin算法(英文:Rabin–Karp algorithm或Karp–Rabin algorithm),是一种由理查德·卡普与迈克尔·拉宾于1987年提出的、使用散列函数以在文本中搜寻单个模式串的字符串搜索算法单次匹配。该算法先使用旋转哈希以快速筛出没法与给定串匹配的文本位置,此后对剩余位置可否成功匹配进行检验。此算法可推广到用于在文本搜寻单个模式串的全部匹配或在文本中搜寻多个模式串的匹配。设计

维基百科3d

按照惯例,对于被匹配的字符串称之为彻底字符串,用于查找匹配的字符串称为为模式字符串。KR算法是经过计算散列值的方式从彻底字符串中进行模式字符串的匹配,也就是咱们常常说的哈希值。code

KR从彻底字符串的首位开始,计算和模式字符串长度一致的子字符串的哈希值,再经过哈希值与模式字符串计算获得的哈希值进行比较,若是哈希值不存在则字符串必定不相等。若是哈希值相等,两个字符串可能相等,这个时候就须要经过遍历对比两个字符串的每一个字符,若是全部顺序字符都相等的话,则两个字符串相等。递归

为何哈希值相等,可是值不必定相等,这里涉及到一个概念就是哈希碰撞,了解的童鞋直接跳过,不了解的童鞋听我举个例子,一年有365天,若是这个时候一个房间里有366我的,那么是否是必定会有两我的的生日的同一天,虽然生日相同,可是不是同一我的,其实哈希能够当作是固定长度的函数,而实际长度大于这个固定长度,因此值会重合,固然这个例子不是特别的准确,感兴趣的童鞋能够维基或者百度更准确的定义。

所以当两个长度同样的字符串计算出的哈希值一致的时候,还须要比对字符串对应位置上的全部字符,所以能够很简单的得出KR算法的实现代码。

func KarpRabinMatch(allString, modeString string) int {
  //计算模式字符串的哈希值
  hashMode := hash(modeString)
  //下标匹配结束
  end:= len(allString)-len(modeString)+1
  for i := 0; i < end ; i++ {
    //计算子字符串的哈希值
    hashKey := hash(allString[i : i+len(modeString)+1])
    if hashMode == hashKey {
      for j := 0; j < len(modeString); j++ {
        if allString[i+j] != modeString[j] {
          break
        }
      }
      return i
    }
  }
  return -1
}

能够看到代码中对模式字符串哈希值(hashMode)的计算只会处理一次,在循环中,从彻底字符串的第一个字符开始的子字符串,计算对应哈希值,判断该哈希值与hashMode比较,若是不相等日后一位计算下一个子字符串的哈希值。

只要哈希值相等的状况下才会对比模式字符串的每个字符,因此选择一个好的哈希函数,会使比较模式字符串每一个字符的操做变得很是少,所以这个算法的时间复杂度在计算子字符串的哈希值上。若是子字符串的每一个字符都要参与计算,彻底字符串的全部字符须要计算长度n遍,每遍须要计算模式字符串的长度m个字符,所以时间复杂度为O(mn)。

02

旋转哈希

如上所说,若是每一次都要对子字符串的每一个字符都进行计算,那么时间复杂度会达到O(mn),若是想要下降时间复杂度(提速),须要找到一种哈希方式,减小每次哈希计算的次数。因而针对这个字符串匹配算法,设计了一种简单的可是不优秀的哈希函数计算方式:旋转哈希。

    旋转哈希(也称为滚动哈希、递归哈希、滚动校验和或滑动哈希)是一种哈希函数,输入的内容在一个窗口中进行移动哈希。    

    少数哈希函数容许快速计算滚动哈希值 — 只给出旧的哈希值,新的哈希值被快速计算出来,旧的值从窗口中移除,新的值添加到窗口中 — 相似于移动平均函数的计算方式,比其余低通滤波器更快。

维基百科

旋转哈希的想法很简单,有点相似窗口移动,每次向右移动窗口,把退出窗口的最左边字符的哈希值减掉,而且加上新加入窗口的最右边字符的哈希值,这样就达到了每次经过常数时间计算出哈希值。

以下所示,子字符串ABB的哈希值,hash1 = A + B + B,当窗口移动到BBE这个子字符串的时候,子字符串BBE的哈希值hash3 = hash1 - A + E便可。

图片

可是按照加法的这种方式去实现旋转哈希,产生哈希碰撞的几率很是高,好比说ABB和BBA的哈希值是同样的。这样就会致使不同的字符串的哈希值相等,须要比较每个字符,时间复杂度变高。

03

Rabin指纹

咱们已经肯定经过旋转哈希来实现KR算法,那么有什么更好的旋转哈希的计算方式可以产生更少的碰撞。这里要说的就是迈克尔·拉宾提出的Rabin指纹。

图片

维基百科

Rabin指纹是经过程序解释多项式,经过当前字符串的多项式值,在窗口移动的时候,校验计算新的子字符串的结果值。它能够应用在一些分块数据的校验上,好比说网络传输包的校验和等。

04

多项式散列

计算哈希函数,若是模式字符串长度较长,经过多项式进行计算,可能会出现哈希值超过机器支持长度的状况,因此这边须要进行取余,简单来讲就是在保证散列尽可能平均分布的同时,不让长度溢出。

首先须要了解求余的特性:同余定理,在百度百科或者维基百科均可以找到对应的内容,其中有一条在咱们的计算中会使用到,也就是:

同余式相乘:若a≡b(mod m),c≡d(mod m),则ac≡bd(mod m)

假设字符集大小为x,用质数y进行取模操做,用多项式散列进行哈希计算的表达式为:

图片

即假设当前字符串字符集大小为256,能够设置x = 256,质数 y = 101 来计算(固然设置其余数值也能够),好比说彻底字符串为abcd,而匹配字符串长度为3。

首先计算子字符串abc的哈希值(字母经过ASCI码计算),即

图片

而后字符串窗口移动,计算bcde的哈希值,即

图片

能够发现上面的两个哈希公式是减掉第一位的a的哈希值,而后再加上最后一位a的哈希值,经过旋转哈希的方式来实现常数时间内哈希值的计算。

对比上面两个公式,会发现 hash(abcd)中a的哈希值和下面计算减掉a的哈希值相差一个 %101,根据上面的同余式相乘公式能够获得,结果是一致的。

图片

图片

05

写在最后

KR算法虽然在单字符串的匹配中,算不上优秀的算法,可是若是在字符串中查找N个对应的模式,即多模式搜索中,KR算法的变种AC自动机的效率很是高。

本文涉及到的代码比较少,不少比较严谨的文字也是引用维基百科上面的解释,只是用了一些图片来诠释处理的过程。全文旨在说明一种思路和计算方式(旋转哈希),也许在以后工做中的某些场景会有所应用。

【往期回顾】

编码:震惊,让领导差点脑溢血的字符串匹配算法KMP

编码:horspool字符串匹配,折磨先生又来了

编码:sunday字符串匹配,“愉快”的一天又开始了

编码:Lumuto划分,实现快速选择

编码:前缀树工具

【参考资料】

旋转哈希

https://zh.wikipedia.org/wiki...

Rabin–Karp算法

https://zh.wikipedia.org/wiki...

Rabin指纹

https://zh.wikipedia.org/wiki...

图片

相关文章
相关标签/搜索