数据结构KMP算法

KMP的作用

在学习数据结构串部分的时候,我们经常遇到经典题型,字符串匹配。我们立刻会想到采用双层循环遍历的方式去匹配,即当字串与主串出现字符不匹配的时候,主串回溯到第二个字符,字串回溯到第一个字符再进行一一匹配,以此类推,设字串长度为n,母串长度为m,则最坏的时间复杂度为O(m * n),没有占用多余的空间,因此空间复杂度为O(1)。当字符串非常长时,所需时间就会很长。

步入正题:KMP算法。举一个例子吧:字串为abcabcnm,母串为abcabcdabcabcnm,在匹配时

abcabcdabcabcnm
abcabcnm
abcabcnm

当字串匹配到n时,匹配不成功,因此需要重新匹配,我们发现n前面的字符串abc、abc,因此我们直接使第一个abc与匹配的第二个abc对其进行匹配,就可以减少匹配的次数,减少了时间复杂度。我们怎么知道该向前移动多少位呢,那就需要了解字符串的最长公共前后缀,了解KMP算法的人就知道,其长度就是我们所需的next数组。

最长公共前后缀

了解前缀表与后缀表

前缀表就是字符串当中除了最后一个字符以外的前缀字符串

后缀表就是字符串当中除了第一个字符以外的后缀字符串

以 abca为例

前缀表后缀表

a

a
abca
abcbca

最长公共前后缀

以aabaaf为例:

当指向下表为0的字符时,字符串为a,则最长公共前后缀为0

当指向下表为1的字符时,字符串为aa,则最长公共前后缀为1

当指向下表为2的字符时,字符串为aab,则最长公共前后缀为0

下标012345
字串aabaaf
最长公共前后缀010120

next数组

在上面的解释当中,我们知道了KMP算法当中字串的移动为前面所匹配的字符串的最长公共前后缀。因此我们采用一个next数组存储字串的最长相等前后缀的长度,next数组只与字串相关。

next[i] = j,含义为:下标为i的字符的字符串最长相等前后缀的长度为j

例:ababcnm

下标01234567
字串abcabcnm
next[i]00012300

因此明确next数组的作用:
1.next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度

2.表示该处字符不匹配时应该回溯到的字符的下标

KMP算法的时间复杂度

我们使用KMP算法的时候,先对字串求了next数组,再对主串进行遍历,每次都只使字串与其匹配的位置发生变化。我们设主串s长度为n,字串t的长度为m。求next数组的时间复杂度为O(m),在匹配期间主串不进行回溯,因此比较次数为n,即KMP算法的时间复杂度为O(m + n),空间复杂度为O(m),相比于朴素模式匹配的时间复杂度O(m * n),算法速度提高了许多。

代码分析

next数组的代码

1.定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。然后还要对next数组进行初始化赋值。一开始i为0,则字符串只有一个字符,因此它的最长相等的前后缀长度为0。

next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)所以初始化next[0] = j 。

2.对于循环部分,我们将两个字符进行对比,当两个字符不相同时,也就是遇到前后缀末尾不相同的情况,就要向前回退。

怎么回退呢?

next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。

那么 s[i] 与 s[j] 不相同,就要找 j 前一个元素在next数组里的值(就是next[j - 1])。

3.如果 s[i] 与 s[j] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。注意赋值前先对j进行加一,因为我们所求的是最大相等前后缀的长度。

private void getNext(int[] next, String s) {
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(j) != s.charAt(i)) 
                j = next[j - 1];
            if (s.charAt(j) == s.charAt(i)) 
                j++;
            next[i] = j; 
        }
    }

使用next数组匹配的代码

定义两个下标j 指向子串起始位置,i指向母串起始位置。

那么j初始值依然为0,为什么呢? 依然因为next数组里记录的起始位置为0。​​​​​​​

接下来就是 s[i] 与 t[j]进行比较。

如果 s[i] 与 t[j] 不相同,j就要从next数组里寻找下一个匹配的位置,以上面的求next数组的思想类似。

        int j = 0;
        for (int i = 0; i < haystack.length(); i++) {
            while (j > 0 && needle.charAt(j) != haystack.charAt(i)) 
                j = next[j - 1];
            if (needle.charAt(j) == haystack.charAt(i)) 
                j++;
            if (j == needle.length()) 
                return i - needle.length() + 1;
        }
        return -1;

具体的代码分析在下题的代码标注部分:会有每一步的详解!

例题:

原题连接:

28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

代码如下:

public static int strStr(String haystack, String needle) {
        if (needle.length() == 0){
            return 0;
        } //当我们所查询的字符串为空时,直接返回0
        int[] next = new int[needle.length()]; //next数组存放的是下标为i时最长公共前后缀的字符串长度
        getNext(next, needle); //此函数来求出next数组
        int j = 0;
        for (int i = 0; i < haystack.length(); i++) { //i为遍历的主串的结尾位置,j为所要找的字串的结尾位置
            while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
                j = next[j - 1];
            } //字串进行返回
            if (needle.charAt(j) == haystack.charAt(i)) {
                j++;
            } //当其匹配成功,对j进行加1
            if (j == needle.length()) { //代表找到了字串的结尾位置,则代表找到了字串
                return i - needle.length() + 1; //i为遍历的母串时的尾部位置,而我们所求的是第一个匹配的下标
            }
        }
        return -1; //由题,没有返回,代表needle 不是 haystack 的一部分,则返回-1
    }
    public static void getNext(int[] next, String s) {
        int j = 0; //使用i,j指针分别指向的是字符串的前缀的末尾与后缀的末尾
        next[0] = 0; //一开始,使j初始化为0,因为当i为0时,字符串的最长公共前后缀的字符串长度为0
        //并将其初始化给next[0]
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(j) != s.charAt(i)) {
                j = next[j - 1];
            } //此循环是为了找到最长共前后缀时j的位置,j > 0是因为,当j为0时,j = next[j - 1]数组越界
              //当不匹配时,j会找前面所求的最大公共前后缀的位置,使其返回到那个位置,进行匹配
            if (s.charAt(j) == s.charAt(i)) {
                j++;
            } //当所找的字符串的前缀与后缀一样时,j指向的是前缀的位置,而我们所求的是前缀的长度,因此要自增1
            next[i] = j; //将所求的最长公共前后缀的字符串长度存入数组当中
        }
    }
}

  • 44
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值