28. 实现 strStr()
思路
KMP
KMP是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。
KMP主要应用在字符串匹配上。
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。
next数组就是一个前缀表(prefix table)。前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
比如要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
如动画所示:
可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
最长公共前后缀 (最长相等前后缀)
文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
前缀表求的就是相同前后缀的长度。
前缀表与next数组
很多KMP算法的实现都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
注意next数组是新前缀表(旧前缀表统一减一了)。
匹配过程动画如下:
构造next数组
定义一个函数getNext来构建next数组,函数参数为指向next数组的指针和一个字符串。
代码如下:
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
1. 初始化
定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。
然后还要对next数组进行初始化赋值,如下:
2. 处理前后缀不相同的情况
因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。
所以遍历模式串s的循环下标i 要从 1开始,代码如下:
如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。
怎么回退呢?
next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。
所以,处理前后缀不相同的情况代码如下:
3. 处理前后缀相同的情况
如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
代码如下:
最后整体构建next数组的函数代码如下:
使用next数组来做匹配
- 时间复杂度: O(n + m)
- 空间复杂度: O(m), 只需要保存字符串needle的前缀表
459.重复的子字符串
思路
移动匹配
当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是由前后相同的子串组成。
所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。
- 时间复杂度: O(n)
- 空间复杂度: O(1)
Note: std::string::npos is a constant that holds the largest possible value of size_t type ( 18446744073709551615 on 64-bit systems ), which is an unsigned integer type. Hence, -1 corresponds to the actual value of std::string::npos.