Day09、字符串part02

KMP算法

什么是KMP

  • KMP算法一种改进的字符串的模式匹配算法,是D.E.Knuth、V.R.Pratt、J.H.Morris于1977年联合发表,所以取了三位学者名字的首字母。所以叫做KMP。

什么是字符串的模式匹配?

  • 给定两个串S=“s1s2s3 …sn”和T=“t1t2t3…tn”,在主串S中寻找子串T的过程叫做模式匹配,T称为模式。
    举一个例子:要在文本串:aabaabaafa中查找是否出现过一个模式串:aabaaf。
    正常的暴力思路就是直接将文本串从头开始比较,如果不匹配,则从文本串的第二个字符开始比较模式串,后面以此类推。此时的时间复杂度为O(n × m)。
    那么KMP算法就可以让我们每次遍历不从头开始遍历。
    KMP算法的时间复杂度是O(n+m)的。

什么是前缀表

  • 前缀表就是KMP算法的精髓所在。
  • 还是以上门提到的例子为例。使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。
  • 那么,前缀表是如何记录的呢?
    首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
  • 那么什么是前缀表
    记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
    - 首先,什么是"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
    - 前缀表就是记录字符串中有多大长度的相同前缀后缀。
    以"aabaaf"为例,

"a"的前缀和后缀都为空集,共有元素的长度为0;
"aa"的前缀为[a],后缀为[a],最长共有元素为[a],长度为1;
"aab"的前缀为[a, ab],后缀为[ab, b],共有元素的长度为0;
"aaba"的前缀为[a, aa, aab],后缀为[aba, ba, a],最长共有元素为[a],长度为1;
"aabaa"的前缀为[a, aa, aab, aaba],后缀为[abaa, baa, aa, a],最长共有元素为[aa],长度为2;
"aabaaf"的前缀为[a, aa, aab, aaba, aabaa],后缀为[abaaf, baaf, aaf, af, f],共有元素的长度为0;
那么我们这时就可以得到前缀表为[0,1,0,1,2,0]

看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
在这里插入图片描述
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。
所以要看前一位的 前缀表的数值。
前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。
最后就在文本串中找到了和模式串匹配的子串了。

前缀表与next数组
很多KMP算法的实现都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
拿上面得到的前缀表[0,1,0,1,2,0]为例,next数组(前缀表统一减一后得到)[-1,0,-1,0,1,-1]。
为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。
其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。

使用next数组来匹配
以下我们以前缀表统一减一之后的next数组来做演示。
有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。
注意next数组是新前缀表(旧前缀表统一减一了)。
next数组:[-1,0,-1,0,1,-1]
匹配过程动画如下:
在这里插入图片描述
构造next数组
原理知道之后,那么要用KMP算法,就要得到我们的next数组,那么我们定义一个函数getNext来构建next数组。 代码如下:

    public void getNext(int[] next, String s){
    	//初始化:
		//定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。
		//j 为什么要初始化为 -1呢,因为之前说过 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择j初始化为-1,
        int j = -1;
        next[0] = j;
        
        for (int i = 1; i < s.length(); i++){
            //处理前后缀不相同的情况
            while(j >= 0 && s.charAt(i) != s.charAt(j+1)){
                j=next[j];// 向前回退
            }

			//处理前后缀相同的情况
            if(s.charAt(i) == s.charAt(j+1)){
                j++;
            }
            next[i] = j;// 将j(前缀的长度)赋给next[i]
        }
    }

代码构造next数组的逻辑流程动画如下:
在这里插入图片描述

得到了next数组之后,就要用这个来做匹配了。

使用next数组来做匹配
代码如下:
最后放一道leetcode的题目,来加深理解。

int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.length(); i++) { // 注意i就从0开始
    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
        j = next[j]; // j 寻找之前匹配的位置
    }
    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
        j++; // i的增加在for循环里
    }
    if (j == (t.length() - 1) ) { // 如果j指向了模式串t的末尾,文本串s里出现了模式串t
        return (i - t.length() + 1);
    }
}

28. 找出字符串中第一个匹配项的下标

题目链接

28. 找出字符串中第一个匹配项的下标

题目描述

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

思路

KMP算法

实现代码

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

            if(s.charAt(i) == s.charAt(j+1)){
                j++;
            }
            next[i] = j;
        }
    }
    public int strStr(String haystack, String needle) {
        if(needle.length()==0){
            return 0;
        }

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

        return -1;
    }
}
  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值