代码随想录算法训练营第九天 | 28. 实现 strStr() KMP 459.重复的子字符串 字符串总结 双指针回顾

28. strStr()

KMP

前缀表:

前缀: 包含首字母不包含尾字母的所有子串

后缀: 只包含尾字母不包含首字母的所有子串

求 -- 最长相等前后缀

下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

如何计算前缀表

aabaaf

a: 0  aa: 1 aab: 0 aaba: 1 aabaa: 2 aabaaf: 0 前缀表:01012

前缀表与next数组

很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?

next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。

为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。

其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。

后面我会提供两种不同的实现代码,大家就明白了

使用next数组来匹配

以下我们以前缀表统一减一之后的next数组来做演示

有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。

注意next数组是新前缀表(旧前缀表统一减一了)。

时间复杂度分析

其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。

为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串。

都知道使用KMP算法,一定要构造next数组

构造next数组(我更习惯next不减1的,所以按照不减1来写)

文本串s中找模式串t, j 指向模式串起始位置, i 指向文本串起始

    void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = j;
        //注意 i 从 1 开始
        for(int i = 1; i < s.size(); i++) {
            // 前后缀不同了
            while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
                j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
            }
            // 如果相同
            if (s[i] == s[j]) {
                j++;
            }
            // 将前缀的长度赋给next[i]
            next[i] = j;
        }
    }

strStr()

    def strStr(self, haystack: str, needle: str) -> int:
        if len(needle) == 0:
            return 0
        # get the nextList for needle
        nextL = [0]* len(needle)
        self.getNext(nextL, needle)
        j = 0
        for i in range(len(haystack)):
            # haystack != needle
            while j >0 and haystack[i] != needle[j]:
                j = nextL[j - 1]
            # ==
            if haystack[i] == needle[j]:
                j += 1
            # j reach the end of the needle
            if j == len(needle):
                return i - len(needle) + 1
        return -1

    
    def getNext(self, nextL: List[int], s: str) -> None:
        j = 0
        nextL[0] = j
        for i in range(1, len(s)):
            while j > 0 and s[i] != s[j]:
                j = nextL[j-1]
            if s[i] == s[j]:
                j += 1
            nextL[i] = j

459. Repeated Substring Pattern (KMP)

解法1: 

    def repeatedSubstringPattern(self, s: str) -> bool:
        # 如果是一个字符重复出现,那么把double里面肯定还有一个跟他一样的字符
        new_s = s + s
        new_s = new_s[1: len(new_s) - 1]
        if s in new_s:
            return True
        else:
            return False

解法2:

KMP: 在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示:

数学推导

假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。

因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理)

所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。

  

   def repeatedSubstringPattern(self, s: str) -> bool:
        if len(s) == 0:
            return False
        nxt = [0] * len(s)
        self.getNext(nxt, s)
        if nxt[-1] != 0 and len(s) % (len(s) - nxt[-1]) == 0:
            return True
        return False

    
    def getNext(self, nextL: List[int], s: str) -> None:
        j = 0
        nextL[0] = j
        for i in range(1, len(s)):
            while j > 0 and s[i] != s[j]:
                j = nextL[j-1]
            if s[i] == s[j]:
                j += 1
            nextL[i] = j

字符串

代码随想录

双指针

数组篇

通过两个指针在一个for循环下完成两个for循环的工作 ( slow/fast)

字符串

定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素

很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

链表

reverse/cycle

N数之和篇

通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作

总结

今天花了很多时间 搞懂KMP这个算法,大概掌握了70%,二刷的时候可以回顾一下

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值