算法学习之KMP

算法学习之KMP

  • 前言
  • 一 实现strStr()真的很难
  • 二 实现strStr(),用KMP简单多了!
    • 1.KMP理论篇
    • 2.手撸一个前缀表
    • 3.代码实现
  • 总结


前言

什么是KMP算法?说真的,写下这篇博客的时候我也还没有搭建其这个看似超级高深的算法的知识基础、体系,写下这篇博客,一方面是因为KMP算法对以后参加比赛或者学习思维上能开展更为开阔的思路,也是因为Leetcode上的一道题目,这道题目困惑我许久,今天重拾这道题依旧让我手足无措,我们就这道题目展开,对KMP算法的神秘面纱。

一 实现strStr()真的很难

说真的,在我晚上重新回顾这道题的时候,我以为我轻松的写出来了,思路也很清晰,可是答案结果却是让我不知所措,我们来看看这道题目及我的思路全过程:

28.实现strStr()

实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符

个人思路第一辑:

class Solution {
    public int strStr(String haystack, String needle) {
        /***解答本题:
        1.首先找到匹配的第一个字符
        若符合,则继续寻找【needle往下走1】,直到needle字符串检测完毕,需要注意的是haystack需要一直走1
        若不符合,则需要将needle重新定位到开头,将结果位置定到haystack位置

        可以采用计数的方法,结果为needle的长度,匹配一个加一,直到长度相等,退出,不相等,清0,继续
        2.所有找完,若此时仍然不符合要求,那么返回-1

        测试结果:
        会导致以下错误:
        ①上一个或者前几个有效字符因为某些原因不能重复读入,导致有这么一种字符串但是却无法读入
        如:"mississippi" "issip" 错误:
        */

        int resultLen=0;                    //结果长度
        int resultIndex=0;                  //结果位置

        int needleLen=needle.length();      //结果集所需要达到的长度
        int haystackLen=haystack.length();

        int index=0;
        for(int i=0;i<haystackLen;i++){
            if(haystack.charAt(i)==needle.charAt(index)){
                index++;
                resultLen++;
            }else{
                //清0
                index=0;
                resultLen=0;
            }
            if(resultLen==needleLen){
                resultIndex=i-needleLen+1;
                break;
            }
        }

        return resultIndex==0?-1:resultIndex;
    }
}

是的,个人认为是”天衣无缝“的题解,却会因为字符串“吞噬”导致解答错误,即正确题解的部分字符由于前面不符然后也被删除了:

例如:
mississippi
issip
读入m,失败,到下一个,i->s->s->i,到这里都是正确的,下一个s,错误,所以重新读取,但是此时是从s开始读取,前面的i无法读取了,啊哈,正确答案也就这样消失了!这让我无比困惑

个人思路第二辑:
既然单此循环容易出事,那我就双重循环嘛!这样就可以避免前面符合题解的字符被误删了!代码如下:

class Solution {
    public int strStr(String haystack, String needle) {
        /**
        依然是从左往右读取,与此不同的是,循环是双层嵌套的,这样防止了数据的丢失
        */

        int needleLen=needle.length();
        //特殊情况特殊处理:
        if(needleLen==0)
            return 0;
        int haystackLen=haystack.length();
        if(haystackLen<needleLen)
            return -1;
        int hayIndex=0;     //双重循环内的指针
        int needleIndex=0;  //needle的指针
        int resultLen=0;    //needle当前长度
        int resultIndex=0;  //结果位置

        boolean flag=false;
        for(int i=0;i<haystackLen;i++){
            hayIndex=i;
            while(hayIndex<haystackLen){
                if(haystack.charAt(hayIndex)==needle.charAt(needleIndex)){
                    needleIndex++;
                    resultLen++;
                }else{
                    needleIndex=0;
                    resultLen=0;
                    break;
                }
                if(resultLen==needleLen){
                    resultIndex=hayIndex-needleLen+1;
                    flag=true;
                    break;
                }
                hayIndex++;
            }
            if(flag){
                break;
            }
        }
        // return resultIndex==0?-1:resultIndex;
        if(flag){
            return resultIndex;
        }else{
            return -1;
        }
    }
}

好家伙,确实这是勉强通过的,但是还是开心了一小阵hhhh
算法学习之KMP_第1张图片
这样的解法太勉强了!所以还是需要学习我们所需要讲的KMP算法!

二 实现strStr(),用KMP简单多了!

1.KMP理论篇

KMP算法解决:字符串匹配的问题

①KMP的主要思想:
当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配

②首先要明白的几个概念:

  • 前缀与后缀
  • 最长公共前后缀(最长相等前后缀)
  • 前缀表
  1. 前缀:
    顾名思义,前缀即为一个词的前面部分,比如英语单词:unimaginable,它结构无非就是un+imagin(e)+able,分别对应的就是前缀+词根+后缀,而程序员眼中的前缀又是稍许不同,我们给它下个定义:不包含"最后"一个字符的前面所有的字符子串就是它的前缀
    举个例子:akbakf,f的前缀就是akbak,同理,可以类推后缀

2.最长公共前后缀(最长相等前后缀)
理解完什么是前缀和后缀,那么最长公共前后缀又是什么呢?
我的理解是"对称",对于某一"划线"分割的字符前缀和后缀
比如:akbakf
①对a字符,只有一个,没有前缀和后缀,它们公共部分长度自然为0
②对于ak子串,它前缀是a,后缀是k,好像并不"对称",所以公共部分也是0
③对于akb子串,我们以中间部分划开(以此保证前后缀长度相等),显然它们(a和b)也是不"对称"的
④对于akba子串,同样的方法,我们发现:前缀和后缀都是a,它们是"对称"的
⑤对于akbak子串,同样的方法,我们发现:前缀和后缀都是ak,它们是"对称"的
⑥akbakf,划分后不存在"对称"关系

这里需要注意一下:我这里所述的对称,细心的读者发现都是带着引号的,它并不是数学观念上的对称,读者可以慢慢领会~

前缀表:
上面所求的最长相等前后缀,就是为前缀表所铺垫,因为最长相等前后缀是要解决字符串匹配问题,回收前面的概括就是:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配
是的,匹配过的内容,不正是它们相等的前后缀吗?而前缀表刚好可以帮我们完成这个工作!
那么,如何计算前缀表呢?
其实只需要将上面所讲的最长公共前后缀一一匹配相对应的字符即可得到前缀表,如akbakf,它的前缀表就是[0,0,0,1,2,0](PS:前缀表并不唯一,我们可以有多种定义去解释多种前缀表,这里先将最适合新手入门的一种前缀表)

2.手撸一个前缀表

前面都是理论为主,如何计算得出前缀表呢?
由于博主也是边看教程边学KMP,所以这边引用一下三叶姐的题解思路(三叶姐yyds!)
【宫水三叶:简单题学KMP】

前缀表常常有两种说法:next数组和prefix数组,我们这里使用next来表示它

前后缀相同
算法学习之KMP_第2张图片
j—>前缀末尾
i—>后缀末尾

前后缀不相同:
算法学习之KMP_第3张图片
继续匹配至匹配完成:
算法学习之KMP_第4张图片

算法学习之KMP_第5张图片
整个数组构建的时间复杂度为O(m)
因此KMP算法时间复杂度O(m+n),就是由此得出的

我们再来用代码实现它!

3.代码实现

废话不多说,直接上代码

/***使用前缀表实现strStr()
    计算前缀表:
    1.初始化
    2.前后缀不相同
    3.前后缀相同
    4.更新前缀表
 */
class Solution {
    public void getNext(int[] next,String s){
        int j=0;    //前缀末尾
        next[0]=0;  //初始化next数组,首位一定为0
        for(int i=1;i<s.length();i++){      //i是后缀末尾
            while(j>0&&s.charAt(i)!=s.charAt(j)){
                //处理前后缀不相同
                j=next[j-1];                //j不断前移直至匹配或者为0,遇见冲突看前一位
            }
            //处理前后缀相同情况
            if(s.charAt(i)==s.charAt(j)){
                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);

        //实现strStr()
        int j=0;                    //前缀末尾
        for(int i=0;i<haystack.length();i++){
            while(j>0&&haystack.charAt(i)!=needle.charAt(j)){
                j=next[j-1];        //和得到前缀表相同原则
            }

            //处理前后缀相同
            if(haystack.charAt(i)==needle.charAt(j)){
                j++;
            }

            //达到长度返回结果
            if(j==needle.length())
                return (i-needle.length()+1);
        }
        return -1;  //未找到
    }
}

建议读者可以尝试使用:

文本串:akbakcakcakf
模式串:akcakf

进行模拟

时间复杂度也大大减少了!
算法学习之KMP_第6张图片


总结

理解KMP算法可以让我们在字符串匹配问题中更加得心应手,我们可以用更少的时间完成字符串的匹配,我们也通过这一个简单题了解了前缀表,并且在日后的学习中会经常使用到它
写到最后,也祝大家节日快乐~

你可能感兴趣的:(算法设计,1024程序员节,算法,java)