Leetcode 刷题笔记(八) —— 字符串篇之 KMP

KMP

刷题路线以及 KMP 详解 :代码随想录

什么是 KMP 算法?

KMP 是以 3 位发明者名字的首字母命名的,常用来字符串匹配上。如:判断 s1 是不是 s2 的子串,KMP 算法将解决此问题的时间复杂度从 O(m*n) 降低到了 O(n)
它的思想是当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

如:文本串:aabaabaaf,模式串:aabaaf,判断 aabaaf 是否为 aabaabaaf 的子串
aabaabaaf
aabaaf
当在文本串下标 i = 5 处发现 b 和 f 不匹配时,模式串没有从文本串下标 i = 1 和 i = 2 开始从头比较,
aabaabaaf
. aabaaf
. . aabaaf
而是让下标 i 不回退,改变模式串下次比较的下标,在 aabaaf 的 f处 匹配失败,但是我们发现 f 前边的 aa (. . . aaf 和 开头的 aa. . . .) 相同 而因为已经匹配到 f 说明 f 前的 aabaa(aabaaf) 一定与 文本串(aabaabaaf)的 aabaa 已经完全匹配,那么文本串(aabaabaaf)发生匹配失败的前的 aa 一定在 模式串(aabaaf)的前缀出现。此时就可以直接去比较两个 aa 后边的字符就可以了。
aabaabaaf
. . . aabaaf

next 数组

数组next[i] 保存模式串[0, i] 区间子串的最长相同前后缀的长度,在匹配失败时,用来找到下次应该匹配的位置。最大相等前后缀的长度作为下标刚好是前缀尾部后一个位置,刚好作为匹配失败模式串指针回退的位置。

题录

28. 实现 strStr()

Leetcode 链接
实现 strStr() 函数。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。

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

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

示例 3:
输入:haystack = “”, needle = “”
输出:0

题解: 我们称 haystack = “hello”, needle = “ll” 中前者为文本串,后者为模式串
方式一:暴力解,第一层循环遍历文本串表示相同子串开始位置,第二层循环遍历模式串

class Solution {
    public int strStr(String haystack, String needle) {
        int hLen = haystack.length();
        int nLen = needle.length();
        // 模式串为空,返回0
        if (nLen == 0) return 0;
        // i 表示文本串该位置作为匹配的起始位置,所以 循环到hLen - nLen 结束,因为再往后一个位置作为匹配的起始位置,文本串剩余位置不够模式串匹配
        for (int i = 0; i <= hLen - nLen; i++) {
            if (haystack.charAt(i) == needle.charAt(0)) {
            	// 文本串 i 下标位置和模式串0下标位置已经匹配上,从 i+1 位置开始向后匹配
                int j = 1; 
                // j 指向模式串每轮匹配位置,则文本串每轮匹配位置为 i + j
                while (j < nLen && haystack.charAt(i + j) == needle.charAt(j)) {
                	// 当前字符匹配,j 指向下轮要匹配的位置
                    j++;
                }
                // 如果已经匹配到最后一个位置,表示整个模式串匹配成功,返回本轮匹配的起始位置
                if (j == nLen) return i;
            }
        }
        // 没有匹配成功
        return -1;
    }
}

KMP解法:

class Solution {
    public int strStr(String haystack, String needle) {
        int hLen = haystack.length();
        int nLen = needle.length();
        if (nLen == 0) return 0;
        // j 为模式串指针,表示接下来模式串比较的位置
        int j = 0;
        // 得到模式串对应的 next 数组
        int[] next = getNext(needle);
        // i 为文本串指针,表示接下来文本串比较的位置
        // 这里循环与暴力解法结束位置不同需要注意,因为这里的 i 表示接下来文本串比较的位置,i 要走到头
        for (int i = 0; i < hLen; i++) {
            // 如果不相等,j 回退,而 i不动
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                // j 回退到[0,j)子串的最大相等前后缀的前缀尾后一个位置,如果没有相等的前后缀直接回到0 下标
                j = next[j - 1];
            }
            // 如果相等 j++,循环结束 i也++,表示接下来比较的位置
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
                // 模式串指针 j 已经走到最后,成功匹配
                if (j == nLen) return i - nLen + 1;
            } 
        }
        return -1;
    }
    public int[] getNext(String s) {
        int len = s.length();
        // 1. 初始化
        int[] next = new int[len];
        int j = 0;   // 前缀指针 j=0, 后缀指针 i=1
        // j 代表接下来应该比较的前缀,同时代表这最大相等前后缀长度;i 代表接下来应该比较的后缀
        for (int i = 1; i < len; i ++) {
            // i 接下来需要比较的后缀,同时也表示 next 数组本轮填充位置
            // 不相等时回退
            while (j > 0 && s.charAt(i) != s.charAt(j)) {
                // j 回退到[0,j)子串的最大相等前后缀的前缀尾后一个位置,如果没有相等的前后缀直接回到0 下标
                j = next[j - 1];
            }
            // 相等时 j++,表示最大相等前后缀长度+1,同时指向下一次应该比较的前缀位置
            if (s.charAt(i) == s.charAt(j)) {
                j++;
            }
            // next数组 i 下标填充 [0, i] 子串的最大相等前后缀长度
            next[i] = j;
        }
        return next;
    }
}

459. 重复的子字符串

Leetcode 链接
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = “abab”
输出: true
解释: 可由子串 “ab” 重复两次构成。

示例 2:
输入: s = “aba”
输出: false

示例 3:
输入: s = “abcabcabcabc”
输出: true
解释: 可由子串 “abc” 重复四次构成。 (或子串 “abcabc” 重复两次构成。)
题解:
暴力解法:以前 i 个字符作为子串是否能被重构,第一层循环遍历所有 i 的可能,第二次循环使用双指针判断能否被重构

    public boolean repeatedSubstringPattern(String s) {
        int len = s.length();
        // i 表示子串长度,从 1 到最大 len/2
        for (int i = 1; i <= len / 2; i++) {
        	// 如果子串长度为 len 的因数,表示可能被 [0,i) 区间子串重构
            if (len % i == 0) {
                int j = 0;
                // 双指针 j 和 j + i 位置逐个比较
                for (; j < len - i; j ++) {
                    if(s.charAt(j) != s.charAt(j + i)) {
                    	// 一旦必相等,表示不能被 [0,i) 区间子串重构
                    	break;
                    }
                }
                // 如果是因为越界结束循环,表示可以被重构
                if (j + i == len) return true;
            }
        }
        return false;
    }

巧解:
举个栗子: 如果能被子串重构,那么原字符串经过循环移位后一定能等于原字符串
abab
. baba
. . abab
abababab 也就是说满足原字符串是两个原字符串拼接再一起去头去尾 bababa 的子串

    public boolean repeatedSubstringPattern(String s) {
	    String str = s + s;
	    return str.substring(1, str.length()-1).contains(s);
    }

KMP解法:
举个栗子:
s = “abcabcabcabc” 对应 next 数组为:000 123 456 789,s.length() = 12 = 9(next 数组最后一位) + 3(重构子串长度),3 是 s长度的因数,所以 s 能被子串重构

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int len = s.length();
        int[] next = new int[len];
        // 前缀指针 j
        int j = 0;
        // 填充 next 数组
        for (int i = 1; i < len; 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;
        }
        // 子串长度不能等于 s 长度
        if (next[len - 1] == 0) return false;
        // 子串长度:len - next[len - 1] 
        return (len % (len - next[len - 1]) == 0);
    }
}
上一篇:LeetCode 537. 复数乘法(Complex Number Multiplication)


下一篇:利用kaptcha生成登录验证码