28 实现 strStr()
自己写:暴力解法
思路:
- 如果长度needle更大则先返回.
- 然后在haystack设置cur1指针遍历字符串,找与needle[0]相等的
- 没找到则返回-1
- 找到与needle[0]相等元素则一一往后匹配,并用temp记录此时cur1位置
- 匹配成功则返回cur1
- 匹配失败则让cur1 = temp继续遍历
public int strStr(String haystack, String needle) {
// 思路:如果长度needle更大则先返回.
// 然后在haystack设置cur1指针遍历字符串,
// 没找到则返回-1
// 找到与needle[0]相等元素则一一往后匹配,并用temp记录此时cur1位置
// 匹配成功则返回cur1
// 匹配失败则让cur1 = temp继续遍历
// 如果长度needle更大则先返回-1
if (needle.length() > haystack.length()) return -1;
int temp = 0;
// 遍历haystack
for (int cur1 = 0; cur1 < haystack.length(); cur1++) {
// 找到相同单词
if (haystack.charAt(cur1) == needle.charAt(0)){
// 记录匹配成功起点
temp = cur1;
/*System.out.println("进入循环");
System.out.println("此时,cur1为:" + cur1);*/
// 继续匹配全部字母
for (int cur2 = 0; cur2 < needle.length(); cur2++,cur1++) {
if (cur1 >= haystack.length()) return -1;
System.out.println("cur1:" + cur1);
System.out.println("cur2:" + cur2);
// 发现部分不匹配
if (haystack.charAt(cur1) != needle.charAt(cur2)) {
/*System.out.println("我运行了吗");*/
// 重回原始状态
cur1 = temp;
break;
}
// 判断是否已经匹配完成 若匹配完成返回最先temp
if (haystack.charAt(cur1) == needle.charAt(cur2) && cur2 == needle.length() - 1){
return temp;
}
/*System.out.println("循环结束.此时temp为" + temp);
System.out.println("cur1为:" + cur1);*/
}
}
}
return -1;
}
时间复杂度:O(n * m)
看视频:KMP算法
KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
通俗理解:运用已经匹配过的字符,防止再次从头开始匹配 KMP算法难点:如何运用匹配过的字符 - 记录已匹配的字符 - next数组
什么是next数组
next数组是一个前缀表,用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。 创造了next数组后达到以下效果:防止再次从头开始匹配
可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。
- 如果暴力匹配,发现不匹配,此时就要从头匹配了。
- 但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。
可以看出,前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
因此,前缀表的定义为:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
如何求next数组:求最长相等前后缀得前缀表
简单概念:
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
a b a b
f
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
ab a b f
为什么一定要用前缀表
✅简单理解:当匹配失败的时候,使用前缀表能知道回退到哪 ✅详细: 这就是前缀表,那为啥就能告诉我们 上次匹配的位置,并跳过去呢? 回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:
然后就找到了下标2,指向b,继续匹配:如图:
以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
如何求next数组:
理论:从前往后 与 从后往前 n个一致即可 对于a a b a a ,可分成
- a 0
- aa 1
- aab 0
- aaba 1
- aabaa 2
代码实现:
由于初学,只能理解到理论学习。代码实现写出来勉强得懂,但没真正理解透。二刷见~此部分未完持续...
字符串总结
不迷恋库函数
在344.反转字符串中,反转字符串实现较简单,但
打基础的时候,不要太迷恋于库函数。
建议如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。
反转系列
- 当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
541. 反转字符串II中,为了处理逻辑:每隔2k个字符的前k的字符,搞一个计数器,来统计2k,再统计前k个字符。
其实当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
int end = Math.min(ch.length - 1, start + k - 1);可以防止越界
- 局部整体的反转
- 在151.翻转字符串里的单词中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。
这道题目通过 先整体反转再局部反转,实现了反转字符串里的单词。
- 在剑指Offer58-II.左旋转字符串**中,我们通过先局部反转再整体反转达到了左旋的效果。 **
可以结合我的博客第八天中“剑指Offer58-II.左旋转字符串”部分[1,2 ] -> [-1, -2] -> [-2, -1]理解先局部后整体,再理解先整体后局部更容易
双指针法回顾
字符串
在344. 反转字符串中原地反转字符串
- 使用双指针法,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。,时间复杂度是O(n)。
链表的
翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。
- 在 206. 反转链表中,讲如何使用双指针法来翻转链表,只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。
- 在142. 环形链表II中,链表中求环,应该是双指针在链表里最经典的应用,难点是如何通过双指针判断是否有环,而且还要找到环的入口。 当然也需要一些数学基础。
- 使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
N数之和:
通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。
哈希法可以解决 梦开始的地方 —— 1. 两数之和的问题 其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了,(但其实也能用哈希法算到下标后返回对应的值,效率也是快的)
然而,在15. 三数之和中,哈希法并不适用! 使用哈希法的过程中要把符合条件的三元组放进HashSet中,然后在去去重,这样是非常费时的,很容易超时,这失去了哈希法的优势。
所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。
双指针难点:剪枝去重 只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。
在18. 四数之和中,讲其实思路是一样的,在三数之和的基础上再套一层for循环,依然是使用双指针法。 对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。 同样的道理,五数之和,n数之和都是在这个基础上累加。
总结:
字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。 双指针法是字符串处理的常客。 KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易。需要进一步学习。
学习资料: