时间复杂度O(n*m),其中n是主串长度,m是模式串长度。
缺陷:忽略了已检测过的文本信息。
如果模式串长度为m,主串长度为n,那在主串中,就会有n-m+1个长度为m的子串。
BF算法需要对比n-m+1次,每次对比都需要依次对比m个字符。
RK算法的思路是:
简单的哈希算法,需要遍历子串的每个字符,尽管模式串与子串比较的效率提高了,但是,算法整体的效率并没有提高。
巧妙的设计哈希算法。假设要匹配的字符串的字符集中只包含K个字符,我们可以用一个K进制数来表示一个子串,这个K进制数转化成十 进制数,作为子串的哈希值。
可以提前计算26^(m-1)这部分的值,然后通过查表的方式提高效率。
RK算法包含两部分,计算子串哈希值和模式串哈希值与子串哈希值之间的比较。
所以,RK算法整体的时间复杂度就是O(n)。
如上这种哈希算法是不会有哈希冲突的,因为我们是基于进制来表示一个字符串的,也就是说,一个字符串与一个二十六进制数一一对应,不同的字符串的哈希值肯定不一样。
问题:模式串很长,相应的主串中的子串也会很长,通过上面的哈希算法计算得到的哈希值就可能很大,可能会超过了计算机中整型数据可以表示的范围。
建议:设计数值较小的哈希函数,可能会有哈希冲突。在哈希值相等时,还需再对比一下子串和模式串本身。
我们把模式串和主串的匹配过程,看作模式串在主串中不停地往后滑动。当遇到不匹配的字符时,BF算法和RK算法的做法是,模式串往后滑动一位,然后从模式 串的第一个字符开始重新匹配。
在这个例子里,主串中的c,在模式串中是不存在的,所以,模式串向后滑动的时候,只要c与模式串有重合,肯定无法匹配。所以,我们可以一次性把模式串往 后多滑动几位,把模式串移动到c的后面。
字符串匹配的关键,就是模式串的如何移动最高效。
BM算法本质上就是寻找一种规律,使得模式串和主串匹配过程中,当遇到字符不匹配的时候,能够跳过一些肯定不会匹配的情况,将模式串尽量往后多滑动几位。
BM算法包含两部分
从模式串的末尾往前倒着匹配,当我们发现某个字符没法匹配的时候。我们把这个没有匹配的字符叫作坏字符(主串中的字符)。
利用坏字符规则,BM算法在最好情况下的时间复杂度非常低,是O(n/m)。
比如,主串是aaabaaabaaabaaab,模式串是aaaa。每次比对,模式串都可以直接后移四位,所以,匹配具有类似特点的模式串和主串的时候,BM算法非常高效。
不过,单纯使用坏字符规则还是不够的。因为根据si-xi计算出来的移动位数,有可能是负数,比如主串是aaaaaaaaaaaaaaaa,模式串是baaa。不但不会向后滑动模式串,还有可能倒退。所以,BM算法还需要用到“好后缀规则”。
我们把已经匹配的bc叫作好后缀,记作{u}。我们拿它在模式串中查找,如果找到了另一个跟{u}相匹配的子串{u*},那我们就将模式串滑动到子串{u*}与主串 中{u}对齐的位置。
“坏字符规则”本身不难理解。当遇到坏字符时,要计算往后移动的位数si-xi,其中xi的计算是重点,我们如何求得xi呢?
如果我们拿坏字符,在模式串中顺序遍历查找,这样就会比较低效,势必影响这个算法的性能。
假设字符集为256,每个字符大小为1个字节,可用大小为256的数组,记录每个字符在模式串中出现的位置,数组下标对应字符的ASCII码值,数组值为字符在模式串中的下标。
如上过程翻译成代码如下,其中变量b是模式串,m是模式串长度,bc是刚刚讲的哈希表:
const SIZE int = 256
func generateBC(b []byte, m int, bc []int) {
for i := 0; i < SIZE; i++ {
bc[i] = -1//初始化bc
}
for i := 0; i < m; i++ {
ascii := int(b[i])//计算b[i]的asccii值
bc[ascii] = i
}
}
func bm(mainStr, modeStr []byte) int {
bc := make([]int, SIZE)
n := len(mainStr)
m := len(modeStr)
generateBC(modeStr, m, bc) //构建坏字符哈希
i := 0 //i表示主串与模式串对齐的第一个字符位置
for i <= n-m {
j := 0
for j = m - 1; j >= 0; j-- { //模式串从后往前匹配
if mainStr[i+j] != modeStr[j] {
break //坏字符对应模式串中的下标是j
}
}
if j < 0 {
return i //匹配成功,返回主串与模式串第一个匹配的字符的位置
}
moveNum := j - bc[int(mainStr[i+j])]
if moveNum <= 0 { //坏字符可能产生负数的移位
moveNum = 1
}
i = i + moveNum
}
return -1
}
回顾一下,前面讲过好后缀的处理规则中最核心的内容:
若这两个操作直接使用“暴力”匹配,会使得BM算法效率不高。而好后缀也是模式串本身的后缀子串,所以在正式匹配之前,可以对模式串做预处理操作,预先计算好模式串中的每个后缀子串,对应的另一个可匹配子串的位置。
现在引入suffx数组,数组下标k表示后缀子串的长度,下标对应的数组值表示,在模式串中跟好后缀{u}相匹配的子串{u*}的起始下标位置。
suffx数组可以解决好后缀在模式串中能找到另一个可匹配的情况,但是我们还要在好后缀的后缀子串中,查找最长的能跟模式串前缀子串匹配的后缀子串。
func generateGS(modeStr []byte, suffix []int, prefix []bool) {
m := len(modeStr)
for i := 0; i < m; i++ {
suffix[i] = -1 //默认是找不到和好后缀匹配的子串
prefix[i] = false //初始化
}
for i := 0; i < m-1; i++ { //modeStr[:i],即前缀子串
j := i
k := 0 //公共后缀子串长度
for j >= 0 && modeStr[j] == modeStr[m-k-1] { //与modeStr[:m-1]求公共后缀
j--
k++
suffix[k] = j + 1 //j+1表示公共后缀子串在modeStr[:i]中的起始下标,当有多个{u},suffix[k]会被后来者覆盖
}
if j == -1 {
prefix[k] = true //表示有和后缀子串匹配的前缀子串
//cabcabcab的suffix[3]=3,prefix[3]为true
}
}
}
有了这两个数组后,在模式串和主串匹配过程中,遇到不能匹配字符时,根据好后缀规则,移动过程如下:
func bm(mainStr []byte, modeStr []byte) int {
bc := make([]int, SIZE)
n := len(mainStr)
m := len(modeStr)
generateBC(modeStr, m, bc) //构建坏字符哈希
suffix := make([]int, m)
prefix := make([]bool, m)
generateGS(modeStr, suffix, prefix)
i := 0 //i表示主串与模式串对齐的第一个字符位置
for i <= n-m {
j := 0
for j = m - 1; j >= 0; j-- { //模式串从后往前匹配
if mainStr[i+j] != modeStr[j] {
break //坏字符对应模式串中的下标是j
}
}
if j < 0 {
return i //匹配成功,返回主串与模式串第一个匹配的字符的位置
}
x := j - bc[int(mainStr[i+j])] //坏字符规则算出来的移动位数
y := 0
if j < m-1 { //如果有好后缀(j = m-1时,表示没有好后缀)
y = moveByGS(j, m, suffix, prefix) //返回好后缀规则下,模式串移动的位数
}
i = i + mathutil.Max(x, y) //坏字符规则和好后缀规则,取移动位数更多的
//这里i一定大于0,因为若无好后缀,bc[int(mainStr[i+j])]为-1,那坏字符规则得到的移动位数一定大于0
}
return -1
}
//j表示坏字符对应的模式串中的字符下标,m表示模式串长度
func moveByGS(j, m int, suffix []int, prefix []bool) int {
k := m - 1 - j //好后缀长度
if suffix[k] != -1 { //模式串中存在和好后缀匹配的子串
return j - suffix[k] + 1
}
//这个for循环就是遍历好后缀的后缀子串,看是否存在prefix[m-r]为true的情况
for r := j + 2; r <= m-1; r++ {
//j+2若为m,此时表示好后缀长度为1,此时移动m位即可
if prefix[m-r] == true { //m-r就是好后缀子串的长度
return r
}
}
//若好后缀的两个规则都不命中,则移动m位
return m
}
KMP算法提前构建一个数组,用来存储模式串中每个前缀(这些前缀都有可能是好前缀)的最长可匹配前缀子串的结尾字符下标。我们把这个数组定义为next数组。
func getNext(modeStr []byte) []int {
m := len(modeStr)
next := make([]int, m)
next[0] = -1 //表示好前缀长度为1,无后缀,next[0]为-1
k := -1 //初始为-1,表示最开始匹配时,无好前缀一说,还是要逐个字符匹配
for i := 1; i < m; i++ {
//如下这个for循环,即非简单+1的情况下,依次找next[k]的最长可匹配前缀子串的结尾字符下标
for k != -1 && modeStr[k+1] != modeStr[i] { //k!=-1表示当前已有好前缀
k = next[k] //相当于求modeStr[:k]的最长可匹配前缀子串
//为什么是k=next[k],从上图看,y即是这里的k,已知next[i-1]=k,求next[i]
//因为modeStr[k+1]不等于modeStr[i],故只能看modeStr[:x](x取值为k-1到0)是否与modeStr[i-x:i]是否匹配
//常规操作是比较modeStr[k-1]是否等于modeStr[i],若不等,再看k-2,依次顺序比较
//假设modeStr[k-1]等于modeStr[i],那么还要看modeStr[:k-2]是否匹配modeStr[i-k+1:i-1],若不满足
//则不行,还得往下找,既然一定要满足这个条件,那么可以利用next[k]求出下一个
//最长可匹配前缀子串的位置,再比对下一个字符是否等于modeStr[i]
}
//如下情况,要么是k=-1,要么是modeStr[k+1] == modeStr[i]
if modeStr[k+1] == modeStr[i] {
k++
}
next[i] = k
}
return next
}
func kmp(mainStr, modeStr []byte) int {
n := len(mainStr)
m := len(modeStr)
next := getNext(modeStr)
j := 0
for i := 0; i < n; i++ { //i代表主串中与模式串首字符对齐的位置
for j > 0 && mainStr[i] != modeStr[j] {
j = next[j-1] + 1 //更新j为模式串下次开始匹配的字符位置
//next[j-1]表示当前好前缀是modeStr[:j-1]
}
if mainStr[i] == modeStr[j] { //j代表模式串中坏字符位置
j++
}
if j == m { //主串中找到匹配串
return i - m + 1
}
}
return -1
}
https://leetcode-cn.com/tag/string/