KMP算法

本文详细介绍了KMP字符串匹配算法的核心——next数组的计算和优化过程,包括部分匹配值的概念、next数组的构建以及算法的改进。通过实例分析了next数组在匹配过程中的作用,纠正了书上代码的错误,并提供了从0开始计算的正确代码实现。此外,还推荐了一个KMP算法的速成视频。
摘要由CSDN通过智能技术生成


kmp算法本身匹配的方法理解感觉不太难,最大的难点可能就是next数组的计算,不容易理解。匹配的原理就是,提前算出子串的一个next数组,这个next数组记录了子串中到当前位置的字符串的最长前后缀(前后部分的相同部分的最长长度),利用这个next表,当子串和主串比对的时候,如果不同,就会找next表,利用这个最长长度,计算移动到对应的位置,使得最长前面的部分不用在去匹配,用来提高匹配效率。本文是参考的《王道考研数据结构》上写的,只是记录的一些自己的理解哈。

前缀,后缀,部分匹配值

前缀:除最后一个字符以外,字符串的所有头部子串

后缀:除第一个字符以外,字符串的所有尾部子串

部分匹配值:字符串的前后缀的最长相等前后缀长度

以 “ababa”为例

字符串前缀后缀部分匹配值
a0
ab{a}{b}0
aba{a,ab}{a,ba}1
abab{a,ab,aba}{b,ab,bab}2
ababa{a,ab,aba,abab}{a,ba,aba,baba}3

部分匹配值 = (前缀集合 ∩ 后缀集合 ) 的长度

恭喜!这里next数组的雏形就出来了,说实话这就是next数组。真正最后用的也是在这个基础上优化出来的Next数组。接下来就是使用这个数组,和如何优化。

Sababa
next00123

部分匹配值的使用

主串:a b a b c a b c a c b a b

子串:a b c a c

首先拿到next数组

序号12345
Sabcac
next00010

第一次匹配:

ababcabcacbab

abc

a 和 c不匹配。 前面 ‘ab’ 匹配。最后一个匹配字符b对应的匹配值为0

按照 : 移动位数 = 已匹配的字符数 - 对应的部分匹配值

得出: 移动位数 = 2 - 0 = 2

第二次匹配:

ababcabcacbab

​ abcac

b 和 c不匹配。前面’abca’匹配,最后一个匹配字符a对应的匹配值为1

移动位数 = 4 - 1 = 4

第三次匹配:

ababcabcacbab

​ abcac

子串比对完成。主串没有回退,所以KMP算法时间复杂度为O(n + m)

算法改进

当前的算法

移动位数 = 已匹配的字符数 - 对应的部分匹配值

转为伪代码。

Move = (j - 1) - next[j - 1]
//这里 j的值是对应上面的next表里的序号,如果代码里面数组是从0开始算的话。就应该是 
Move = j - next[j - 1]

优化

使用部分匹配的时候,每次失败都需要找到前一个元素匹配的部分匹配值,使用起来不太方便。将数组右移一位,这样一旦失败直接取到对应的值

序号12345
Sabcac
next-10001
  • 第一个元素为右移之后空缺,用-1填充。目的是第一位不匹配,向右移动一位。就是:移动位数 = 已匹配的字符数(0) + 1 = 1
  • 最后一个元素溢出,因为根本用不到。

这样上面的式子就改为

//按书上的那个序号算
Move = (j - 1) - next[j]
//数组是从0开始算
Move = j - next[j]

那么计算移动应该到达的位置(子串中指针j应该移动到的位置)

j = j - Move 
  = j - ((j - 1) - next[j]) = next[j] + 1  //书上的
  = j - (j - next[j]) = next[j]			   //从0计算

因为每次j = next[j] + 1每次都要+1,很麻烦,把+1放到数组的值里面。这样一旦不匹配就移动到第next[j]的位置即可。

对于数组是从0开始计算的话,不用这个+1操作了,本身就已经是对应的坐标位置了。

OK,以上就是next怎么来的原因。

接下来就是计算next的代码了。

next数组计算代码

书上的错误

//这段是书上写的程序代码,测试用例:"abcac"
void get_next(String T,int next[]){
    int i = 1, j = 0;
    next[1] = 0;
    while(i < T.length){
        if(j == 0 || T.ch[i] == T.ch[j]){
            ++i;++j;next[i] = j;
        }
        else
            j = next[j];
    }
}

这块部分我照着改成C++,Kotlin的方式写了,得出来的结果都不对。自己推逻辑感觉没问题,但是计算机算出来就是不对。最终的问题还是出在指针的位置问题。因为书上讲的编号都是以1开头来算的,但是字符串可不是从1开始算的啊。导致字符串和next数组根本是错开的。问题就出在 T.ch[i] == T.ch[j]上面。得到的next数组就是 0,1,1,1,1

书上代码纠正

//书上代码更正
void get_next(String T,int next[]){
    int i = 1, j = 0;
    next[1] = 0;
    while(i < T.length){
        if(j == 0 || T.ch[i - 1] == T.ch[j - 1]){
            ++i;++j;next[i] = j;
        }
        else
            j = next[j];
    }
}

按照从0开始计算的代码

从0开始的话就不会出现上面的问题,从1开始逻辑没理清就很容易出现错位问题。

void get_next(String T,int next[]){
    int i = 0, j = -1;
    next[1] = -1;
    while(i < T.length - 1){
        if(j == -1 || T.ch[i] == T.ch[j]){
            ++i;++j;next[i] = j;
        }
        else
            j = next[j];
    }
}
//kotlin版
fun String.getNextArr():Array<Int>{
    val list = Array(this.length){i ->  -1}
    var i = 0
    var j = -1
    while (i < this.length - 1){
        if(j == -1 || this[i] == this[j]){
            ++i
            ++j
            list[i] = j
        }
        else
            j = list[j]
    }
    return list
}

代码讲解

额,这块还是按照书上从1开始的方式讲解吧。首先是初始化,i = 1,j = 0。i是持续向后走的,j指针是来回跳的。j主要作用就是和i进行比较,如果i,j的字符相等,i,j同时向后移动,并将j的当前位置赋给i对应的next元素,为什么这么做?分开说

  • i,j同时向后移动做的事就是为了下一次循环再次比对,是否还是相等
  • 将j的当前位置赋给i对应的next元素,为的是记录当出现不匹配的时候,跳转的位置。同时也是完成了前面说的右移操作。也就是说这个位置是记录在匹配好的时候i位置的后一位,也就是做了++i之后的操作。
  • 还有一个操作就是判断了 j == 0,刚好是第一个的前一个,j = 0是为了方便后面+1直接刚好是第一个,这样只要不匹配,得到的坐标就是第一个的位置,直接子串拉回第一个,从头判断
  • else部分就是当不匹配也不是最初状态的时候,j回退到上一次匹配到的位置,下次循环紧接着之前的位置继续匹配。

KMP全部代码

//kotlin版
fun main(){
    var substr = "abcac"
    val next = substr.getNextArr()
    val str = "ababcabcacbab"
    var i = 0
    var j = 0
    var pos = -1
    while (i + substr.length - j != str.length){
        if(j == -1 || str[i] == substr[j]){
            i++
            j++
        }else{
            j = next[j]
        }

        if(j == substr.length){
            pos = i - j
            break
        }
    }
    println(pos)
}

这块没有什么难度了。大家自行分析吧。

速成KMP,推荐一个视频。

觉得文章有些长,可以看这个b站大佬的KMP视频,讲的非常好。

KMP字符串匹配算法1

KMP字符串匹配算法2


欢迎关注菠萝菠萝卜的博客 (也是我 😜😜😜)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值