[LeetCode] 28. 找出字符串中第一个匹配项的下标
[LeetCode] 28. 找出字符串中第一个匹配项的下标 文章解释
[LeetCode] 28. 找出字符串中第一个匹配项的下标 视频解释
题目:
给你两个字符串
haystack
和needle
,请你在haystack
字符串中找出needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果needle
不是haystack
的一部分,则返回-1
。示例 1:
输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。示例 2:
输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。提示:
1 <= haystack.length, needle.length <= 104
haystack
和needle
仅由小写英文字符组成
自己看到题目的第一想法
1. 双层 for 循环, 第一层指针从长字符串的左边往右遍历, 找到和待匹配字符串第一个字符相同的位置, 从这个位置开始往右循环, 如果发现接下来的连续字符串, 和待匹配的字符串一致, 则认为存在, 否则第一层指针下移一位.
2. leetcode 绝大概率不会让人写这样的暴力算法, 所以一定有其他的解决办法. 可以考虑双指针吗? 可不可以这样呢: 当第二层循环遇到相异的字符串时, 可以从第一层循环的当前索引的下一位 (i + 1) 开始, 找到第一个和待匹配字符串第一个字符相同的位置, 这时第一层的索引 i 也跟新到对应的位置. 但是这样好像和双指针的想法并无关联, 因为右侧的指针实际上并没有意义, 除非能判断i到右侧指针之间的字符串, 和待匹配字符串的头几位是相同的. 然而怎么判断呢?
3. 好吧, 第一次看到题目之前我其实没有第 2 点那么多的想法. 因为文章解释里说了, 这是 kmp 算法, kmp 很难, 别想一次搞懂. 先看视频和文章, 知道个大概, 二刷时再来真正掌握吧!
看完代码随想录之后的想法
1. 真的挺难的, 很多细节一直搞不明白
2. 我理解的 kmp 算法:
如果长的字符串中, 存在 n 个连续的字符串, 和待匹配的字符串的头几位相同. 那么当长字符串中这 n 个连续的字符串后一位和待匹配的字符串的下一位不匹配, 例如长字符串头几位为 aabaabaaac 而待匹配字符串为 aabaaac, 当待匹配到待匹配字符串的c(aabaab=>aabaaa)时, 这时候字符串 c 前面的 aabaa 还是相同的. 于是要找到 aabaa 的从尾字符a开始往左以及从头字符a开始往右的连续字符串中, 完全相同的字符串里, 最长的那一串(假设长度为 n). 找到这个串之后, 表示长字符串中当前索引的前 n 位, 和待匹配字符串从头开始的前 n 位是一样的. 因此只需要将长字符串当前索引的字符串, 和待匹配字符串从头开始的第 n + 1 位匹配就可以. 对于 aabaab =》 aabaaa 这样在最后一位 b 和 a 不相同的情况时, 就跳过了 aabaab 中的 第一个 ab 的位置. 提高了匹配的效率.
3. kmp 算法还是挺绕的, 没打算想的太明白, 因为是在耗费了我太多时间和精力了T_T
// 解法一: leetcode 的 1 ms 区
class Solution {
public int strStr(String haystack, String needle) {
if (haystack == null || needle == null
|| haystack == "" || needle == ""
|| haystack.length() < needle.length()) {
return -1;
}
int[] next = getNext(needle);
int currentPos = 0;
for (int i = 0; i < haystack.length(); i++) {
while (currentPos > 0 && haystack.charAt(i) != needle.charAt(currentPos)) {
currentPos = next[currentPos - 1];
}
if (haystack.charAt(i) == needle.charAt(currentPos)) {
if (currentPos == needle.length() - 1) {
return i - needle.length() + 1;
}
currentPos++;
}
}
return -1;
}
private int[] getNext(String pattern) {
if (pattern == null || pattern == "") {
return new int[0];
}
int[] next = new int[pattern.length()];
next[0] = 0;
int prefixIndex = 0;
for (int i = 1; i < pattern.length(); i++) {
while (prefixIndex > 0 && pattern.charAt(i) != pattern.charAt(prefixIndex)) {
prefixIndex = next[prefixIndex - 1];
}
if (pattern.charAt(i) == pattern.charAt(prefixIndex)) {
prefixIndex++;
}
next[i] = prefixIndex;
}
return next;
}
}
// 解法一微调版, 可以进入 leetcode 的 0ms 区
class Solution {
private int[] next;
public int strStr(String haystack, String needle) {
if (haystack == null || needle == null) {
return -1;
}
if (needle == "") {
return 0;
}
char[] haystackChars = haystack.toCharArray();
char[] needleChars = needle.toCharArray();
next = getNext(needleChars);
int needleCharsIndex = 0;
for (int i = 0; i < haystackChars.length; i++) {
while (needleCharsIndex > 0 && haystackChars[i] != needleChars[needleCharsIndex]) {
needleCharsIndex = next[needleCharsIndex - 1];
}
if (haystackChars[i] == needleChars[needleCharsIndex]) {
if (needleCharsIndex == needleChars.length - 1) {
return i - needleChars.length + 1;
}
needleCharsIndex++;
}
}
return -1;
}
public int[] getNext(char[] wordChars) {
if (wordChars == null || wordChars.length == 0) {
return new int[0];
}
int[] next = new int[wordChars.length];
next[0] = 0;
int j = 0;
for (int i = 1; i < wordChars.length; i++) {
if (j > 0 && wordChars[i] != wordChars[j]) {
j = next[j - 1];
}
if (wordChars[i] == wordChars[j]) {
j++;
}
next[i] = j;
}
return next;
}
}
题目:
给定一个非空的字符串
s
,检查是否可以通过由它的一个子串重复多次构成。示例 1:
输入: s = "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。示例 2:
输入: s = "aba" 输出: false示例 3:
输入: s = "abcabcabcabc" 输出: true 解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)提示:
1 <= s.length <= 104
s
由小写英文字母组成
自己看到题目的第一想法
遍历长字符串, 获取以第一个字符串开头的长度小于长字符串长度一半的所有子串, 再判断长字符串能否用这些子串重复组合起来.
同样, leetcode 一定不会是简单的暴力解法.
看完代码随想录之后的想法
解法一:
神奇的想法来了: 如果字符串 s 是由一个子串重复 n 次得到的结果, 那么 s + s 后得到的 ss, 依然可以由子串重复 2n 次得到. 同时 ss 去头去尾后, 还会包含一个完整的 s.
但是我对于 ss.contains(s) 的解法有一点不理解, s 是由子串重复拼凑而来可以推出 ss 去头去尾包含 s, 但是 ss 去头去尾包含 s 一定可以推出 s 是由子串重复拼凑而来吗?
这里可以想一下, 当ss掐头去尾后, ss‘ 包含 s 的部分, 一定是在ss 的中间(不包括头尾), 并且新的 和 s 相等的子串, 头在第一个 s 的尾巴左边, 尾巴在第一个 s 的尾巴右边. 我们把子串的头到第一个 s 的尾巴成为 s0, 把第第一个 s 的尾巴到子串的尾巴成为 s1. 这样我们可以知道, 对于原始的 s 串, s = s0 + s1 = s1 + s0; 我们把 s1 看作短的字符串, 这时 s0 的尾巴也是由 s1 构成的, 因为 s = s0 + s1 = s0‘ + 2s1, 因此可以推出 s0 是由两个 s1 结尾的. 因为 s0 也是由 s1 开头的, 并且可以推出 s0同时是由两个 s1 开头, 因此最终会收敛到 s 是由 n 个 s1 构成的.
解法二:
通过解法一最后一段的推理同时可以知道, 如果 s 剔除最长前后缀后剩下的字符 s1 的长度可以整除 s 的长度, 则 s 一定可以由 s1 通过 n 次循环收敛到 s 的中间.(因为这时候最靠近中心的两个 s1 是刚好相交且不会互相重叠的, 不知道如果相交是什么情况, 还没想明白) 因此求出 s 的最后一个字符的最长前后缀长度后, 通过 s.length() % prefix.length() == 0, 就可以知道 s 是否是由子串循环后构成了.
class Solution {
public boolean repeatedSubstringPattern(String s) {
if (s == null || s.length() < 1) {// 空字符串不能由空字符串组成
return false;
}
// 生成最长前后缀长度表
int[] next = new int[s.length()];
int prefixIndex = 0;
for (int i = 1; i < s.length(); i++) {
while (prefixIndex > 0 && s.charAt(prefixIndex) != s.charAt(i)) {
prefixIndex = next[prefixIndex - 1];
}
if (s.charAt(prefixIndex) == s.charAt(i)) {
prefixIndex++;
}
next[i] = prefixIndex;
}
return next[next.length - 1] > 0 && s.length()%(s.length() - next[next.length - 1]) == 0;
}
}
自己实现过程中遇到哪些困难:
对于最后一个字符没有相同前后缀的时候没有加判断, 导致类似 abac 这样的错误, 因为 "abac".length()%("abac".length() - 0) == 0, 导致错误的认为 abac 也是由子串循环得到了.