刷题笔记:LeetCode28-KMP模式匹配算法拾遗(Java)

先上题目。

1.题目描述

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。

示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2

示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1

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

其实就是手写KMP

(但是已经大四的本人,早就把数据结构学的模式匹配抛之脑后,故而重拾)

我对于解题的习惯是能用暴力先暴力,主要是为了对题目有个初步的了解,再进阶。

2.暴力解法

先上代码。

class Solution {
    public int strStr(String haystack, String needle) {
        int index = -1;
        if(needle == null){
            return 0;
        }
        if(haystack == null){
            return index;
        }
        if(haystack.length() < needle.length()){
            return index;
        }//防止数组越界的条件1
        char[] text = haystack.toCharArray();
        char[] mode = needle.toCharArray();
        for (int i = 0; i < text.length; i++) {
            int temp = i;
            for (int j = 0; j < mode.length; j++,temp++) {
                if(text[temp] != mode[j] || (temp == text.length - 1 && j != mode.length - 1)){
                    break;//后一个条件是为了防止数组越界的条件2
                }
                if(j == mode.length - 1){
                    return i;
                }
            }
        }
        
        return index;
        
    }
}

暴力解法也有陷阱需要避免,比如防止数组越界,以及String为null的情况。

时间复杂度为O(m*n),LeetCode上能够给过,空间复杂度为O(1),(Java比较恶心的地方是不能直接向字符数组一样对String进行操作,我这里不想每次都用String的charAt方法,就转成了char[],空间复杂度就上去了,其实本质上只需要O(1))(其实谈不上恶心,只是因为OOP程度更高,且更安全,毕竟人家还有可扩展的StringBuffer和StringBuilder)。

3.KMP相关前置概念进阶

打BOSS前先刷经验。

3.1 KMP例子

对于模式串为aabaaf,文本串为aabaabaaf,当文本串与模式串都匹配到第六个字母,出现了冲突,此时模式串指针直接回退index为2处(初始为0),即为b字母位置进行接着比对(文本串指针位置不变->可以先感性理解,其实严格证明并不那么想当然)节省了大量时间——相较于暴力解法,文本串不用回退了,模式串不用回退到一开始了

3.2 字符串前缀概念

前缀表示包含首字母,不包含尾字母的所有子串。aabaa的前缀有{a,aa,aab,aaba},没有aabaa

。所有前缀的集合,like,{a,aa,aab,aaba}称之为前缀表

3.3 字符串后缀概念

后缀表示包含尾字母,不包含首字母的所有子串。aabaa的后缀有{a,aa,baa,abaa},没有aabaa

。所有后缀的集合,like,{a,aa,baa,abaa}称之为后缀表

3.4最长相等前后缀

最长相等前后缀指的是->某个固定字符串的后缀表与前缀表的交集中元素的最长长度。e.g.aabaa的最长相等前后缀为{a,aa,aab,aaba}与{a,aa,baa,abaa}的交集{a,aa}的最长长度为aa的长度为2。

是回退位置的决定因素。

3.5 next数组(有的也叫prefix)

next数组是一个模式串等长数组,next[i]维护subString(0,i+1)的最长相等前后缀;

当模式匹配过程中当前字母charAt(i)出现冲突的时候,模式串回退到charAt(next[i-1])。对于next[0]单独写分支。注意next[0] = 0(这是前面3.2和3.3讲前后缀定义时,强调“且”的缘故)

(由于涉及前一位,所以不同人的具体实现与定义会有差异,但是本质都是找最长相等前后缀)

4.算法实现

4.1 求next数组算法实现

i作为后缀的尾指针(从1开始),j作为前缀的尾指针(从0开始)。求next数组的过程相当于前缀和后缀的匹配,文本串为后缀,模式串为前缀。j每轮的起始位置表示上一个i最长相等前后缀的长度。

冲突时,j会返回到next[j - 1]。有人会问为什么不回退到0,原因是,如果回退到0,就无法保证“”最长”。举个例子,“aabaaac”,i= 5,j = 2时,出现不等,如果j回退到0,next[5] = 1,但是实际上应该等于2

    public int[] getNext(String s){
        int[] next = new int[s.length()];
        next[0] = 0;
        int j = 0;//后缀末尾,以及前一位的最长相等前后缀长度
        for (int i = 1; i < next.length; i++) {
            while(s.charAt(i) != s.charAt(j) && j>0){
                j = next[j - 1];//相当于模式匹配时的回退,只不过变成了前缀和后缀的匹配,此时模式串为前缀
            }//注意是while,而不是if
            if(s.charAt(i)==s.charAt(j)){
                j++;
            }
            next[i] = j;
        }

        return next;
    }

4.2 KMP整体实现

class Solution {
    public int strStr(String haystack, String needle) {
        // int index = -1;
        // if(needle == null){
        //     return 0;
        // }
        // if(haystack == null){
        //     return index;
        // }
        // if(haystack.length() < needle.length()){
        //     return index;
        // }
        // char[] text = haystack.toCharArray();
        // char[] mode = needle.toCharArray();
        // for (int i = 0; i < text.length; i++) {
        //     int temp = i;
        //     for (int j = 0; j < mode.length; j++,temp++) {
        //         if(text[temp] != mode[j] || (temp == text.length - 1 && j != mode.length - 1)){
        //             break;
        //         }
        //         if(j == mode.length - 1){
        //             return i;
        //         }
        //     }
        // }
        
        // return index;
        //暴力解法


        //KMP算法
        //前缀表示包含首字母不包含尾字母的所有子串
        //后缀表示包含尾字母不包含首字母的所有子串
        //最长相等前后缀指的是->某个固定字符串的后缀表与前缀表的最长相等者的长度
        /**next数组是一个模式串等长数组,next[i]维护subString(0,i+1)的最长相等前后缀;
        当模式匹配过程中当前字母charAt(i)出现冲突的时候
        模式串回退到charAt(next[i-1])
        **/
        int index = -1;
        int next[] = getNext(needle);
        if(needle == null)return 0;
        if(haystack == null || needle.length() > haystack.length())return index;
        int j = 0;
        for (int i = 0; i < haystack.length(); i++) {
            while(j < needle.length()){
                if(j == 0 && haystack.charAt(i) != needle.charAt(j)){
                    break;
                }  
                if(haystack.charAt(i) == needle.charAt(j) && j == needle.length() - 1){
                    return i - j;
                }//找到的情况
                if(haystack.charAt(i) == needle.charAt(j) && i == haystack.length() - 1 && j < needle.length()){
                    return -1;
                }//末尾部分匹配但是越界的情况
                if(haystack.charAt(i) == needle.charAt(j)){
                    j++;
                    break;
                }
                if(haystack.charAt(i) != needle.charAt(j) && j > 0){
                    j = next[j - 1];
                }
            }
        }
        return index;
    }
    public int[] getNext(String s){
        int[] next = new int[s.length()];
        next[0] = 0;
        int j = 0;//后缀末尾,以及前一位的最长相等前后缀长度
        for (int i = 1; i < next.length; i++) {
            while(s.charAt(i) != s.charAt(j) && j>0){
                j = next[j - 1];//相当于模式匹配时的回退,只不过变成了前缀和后缀的匹配,此时模式串为前缀
                //j = 0;错误
            }//注意是while,而不是if
            if(s.charAt(i)==s.charAt(j)){
                j++;
            }
            next[i] = j;
        }

        return next;
    }
}

时间复杂度会降为O(m+n)

你可能感兴趣的:(笔记,算法,leetcode,java,数据结构)