字符串:KMP

题目:28. 实现 strStr()

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

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

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

思路

BF算法(Brute Force)

将模式串和主串进行比较,一致时则继续比较下一字符,直到比较完整个模式串。不一致时将主串后移一位,模式串则从首位开始对比。

BM算法(Boyer-Moore)


利用 BF 算法,遇到不匹配字符时,每次右移一位模式串,再重新从头进行匹配,但第一次进行字符串匹配时,abcde 都匹配成功,到 x 时失败,又因为模式串每位都不相同,思考不需要再每次右移一位,而是直接跳过这些步骤。

坏字符规则

情况1

BM 算法是从后往前进行比较,发现比较的第一个字符就不匹配,将主串这个字符称之为坏字符(f) 。在发现坏字符之后,模式串 T 中查找是否含有该字符(f),若不存在 f,将模式串右移到坏字符的后面一位。
情况2

继续从右往左进行比较,发现 d 为坏字符,则需要将模式串中的 d 和坏字符对齐。
情况3:模式串中含有多个坏字符

好后缀规则

这种情况不往右移动,甚至还左移。
情况1

红色代表坏字符,绿色代表好后缀。
发现坏字符的时候此时 cac 已经匹配成功,在红色阴影处发现坏字符。此时已经匹配成功的 cac 为好后缀,拿它在模式串中查找,如果找到了另一个和好后缀相匹配的串,那就将另一个和好后缀相匹配的串 ,滑到和好后缀对齐的位置。
情况2:在模式串的头部没有发现好后缀,发现好后缀的子串也可以


总结:

  • 如果模式串含有好后缀,无论是中间还是头部可以按照规则进行移动。如果好后缀在模式串中出现多次,则以最右侧的好后缀为基准
  • 如果模式串头部含有好后缀子串则可以按照规则进行移动,中间部分含有好后缀子串则不可以
  • 如果在模式串尾部就出现不匹配的情况,即不存在好后缀时,则根据坏字符进行移动

KMP算法(Knuth-Morris-Pratt)

KMP算法中移动位数跟主串无关,只跟模式串有关。

最长公共前后缀

移动

next 数组
在next 数组中存前缀的结尾字符下标。

next 数组的使用

next 数组实现
如果P[k] = P[q],那么next[q+1] = k+1,此时表示P[q+1]之前的子串中,存在长度为k+1的相同前后缀。

如果P[k] != P[q],说明P[q+1]之前的子串中,不会存在长度为k+1的相同前后缀。那需要去寻找长度更短的前后缀,假设长度为j,此时P[0]…P[j-1]和P[q-j]…P[q-1]依次相同。

P[0]~P[j-1]一定是以next[q]=k(即P[0]~P[k-1])为长度的那个前缀的一个最长前缀,这是必要条件。

所以P[q]和P[j]不同时,按照k = next[k]递归查找。

代码

BM算法实现

class Solution {
    public int strStr(String haystack, String needle) {
        //i代表主串指针,j模式串
        int i,j;
        //主串长度和模式串长度
        int halen = haystack.length();
        int nelen = needle.length();
        //循环条件,这里只有 i 增长
        for (i = 0 , j = 0; i < halen && j < nelen; ++i) {
            //相同时,则移动 j 指针
            if (haystack.charAt(i) == needle.charAt(j)) {
                ++j;
            } else {
                //不匹配时,将 j 重新指向模式串的头部,将 i 指向本次匹配的开始位置
                i -= j;
                j = 0;
            }
        }
        //查询成功时返回索引,查询失败时返回 -1;
        int renum = j == nelen ? i - nelen : -1;
        return renum;
    }
}

KMP算法实现

class Solution {
    public int strStr(String haystack, String needle) {
        //两种特殊情况
        if (needle.length() == 0) {
            return 0;
        }
        if (haystack.length() == 0) {
            return -1;
        }
        // char 数组
        char[] hasyarr = haystack.toCharArray();
        char[] nearr = needle.toCharArray();
        //长度
        int halen = hasyarr.length;
        int nelen = nearr.length;
        //返回下标
        return kmp(hasyarr,halen,nearr,nelen);

    }
    public int kmp (char[] hasyarr, int halen, char[] nearr, int nelen) {
        //获取next 数组
        int[] next = next(nearr,nelen);
        int j = 0;
        for (int i = 0; i < halen; ++i) {
            //发现不匹配的字符,然后根据 next 数组移动指针,移动到最大公共前后缀的前缀的后一位
            while (j > 0 && hasyarr[i] != nearr[j]) {
                j = next[j - 1] + 1;
                //超出长度时,可以直接返回不存在
                if (nelen - j + i > halen) {
                    return -1;
                }
            }
            //如果相同就将指针同时后移一下,比较下个字符
            if (hasyarr[i] == nearr[j]) {
                ++j;
            }
            //遍历完整个模式串,返回模式串的起点下标
            if (j == nelen) {
                return i - nelen + 1;
            }
        }
        return -1;
    }
    public  int[] next (char[] needle,int len) {
        //定义 next 数组
        int[] next = new int[len];  //next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
        // 初始化,前缀表要统一减一的操作,所以j初始化为-1
        int j = -1;    //j指向前缀终止位置
        next[0] = j;
        for(int i = 1; i < len; i++) { // 注意i从1开始,i指向后缀终止位置
            while (j >= 0 && needle[i] != needle[j + 1]) { // 前后缀不相同
                j = next[j]; // 向前回溯
            }
            if (needle[i] == needle[j + 1]) { // 找到相同的前后缀
                j++;
            }
            next[i] = j; // 将j(前缀的长度)赋给next[i]
        }
        return next;
    }
}
posted @ 2021-04-03 17:37  当康  阅读(68)  评论(0)    收藏  举报