1. LeetCode 8. 找出字符串中第一个匹配项的下标
1.1 思路
- 前缀表的由来:我们遍历到模式串f时发现不匹配,跳到了b位置,为什么呢?因为前面遍历了“aabaa”这个子串,后缀“aa”,前缀“aa”,f不匹配了,就找到与这个子串的后缀“aa”相等的前缀“aa”的后面“b”开始匹配,就是找到这个子串最长的相等前后缀
- 前缀与后缀:前缀是包含首字母,不包含尾字母的所有子串,上述子串只要不是“aabaaf”的都是前缀,只要包括了首字母,不包括尾字母,比如a,aa,aab,aaba,aabaa;后缀是只包含尾字母,不包含首字母的所有子串,上述子串只要不是“aabaaf”的都是后缀,只要包含了尾字母,不包含首字母,比如abaaf,baaf,aaf,af,f
- 最长相等前后缀:a,最长相等前后缀为0,aa,最长相等前后缀为1,aab,最长相等前后缀为0,aaba,最长相等前后缀为1,aabaa,最长相等前后缀为2,aabaaf,最长相等前后缀为0,这些数字就是前缀表,取最长的
1.2 实现步骤
- next数组(前缀表)实现步骤:初始化、处理前后缀不相同的情况、处理前后缀相同的情况、更新
- next数组就是以各个字符串为结尾的这个子串的一个最长相等前后缀的集合
- 初始化:指针i、指针j,指针j指向前缀末尾,i指向后缀末尾,j其实还包括了子串的最长相等前后缀的长度。j初始化为0,因为是从最开始的位置开始,next[0]=0,这个表示当只有一个字符的子串,最大相等前后缀的长度是0,i初始化是进入for循环中,i从1开始到arr.length跟j比较
- 前后缀不相同的情况:s(模式串),s[i]!=s[j]时,j就回退,看j前一位的值对应的下标,就是要回到的位置,j=next[j-1]。为什么看j的前一位的值呢?因为求这个前缀表时遇见冲突也是看前一位的值。由于回退时是连续回退,所以用while
- 前后缀相同的情况:s[i]==s[j],那么j++
- 更新next数组的值:next[i]==j
1.3 代码
class Solution { //前缀表(不减一)Java实现 public int strStr(String haystack, String needle) { if (needle.length() == 0) return 0; int[] next = new int[needle.length()]; getNext(next, needle); int j = 0; for (int i = 0; i < haystack.length(); i++) { while (j > 0 && needle.charAt(j) != haystack.charAt(i)) j = next[j - 1]; if (needle.charAt(j) == haystack.charAt(i)) j++; if (j == needle.length()) return i - needle.length() + 1; } return -1; } private void getNext(int[] next, String s) { int j = 0; next[0] = 0; for (int i = 1; i < s.length(); 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; } } }
2. LeetCode 459. 重复的子字符串
2.1 结论
如果这个字符串是由重复子串组成的,那么重复子串的最小单位就是这个字符串的最长相等前后缀所不包含的那一个子串组成的
2.2 代码
class Solution { public boolean repeatedSubstringPattern(String s) { if (s.equals("")) return false; int len = s.length(); // 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了 s = " " + s; char[] chars = s.toCharArray(); int[] next = new int[len + 1]; // 构造 next 数组过程,j从0开始(空格),i从2开始 for (int i = 2, j = 0; i <= len; i++) { // 匹配不成功,j回到前一位置 next 数组所对应的值 while (j > 0 && chars[i] != chars[j + 1]) j = next[j]; // 匹配成功,j往后移 if (chars[i] == chars[j + 1]) j++; // 更新 next 数组的值 next[i] = j; } // 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值 if (next[len] > 0 && len % (len - next[len]) == 0) { return true; } return false; } }
3. 字符串篇
3.1 双指针法
在344.反转字符串 ,我们使用双指针法实现了反转字符串的操作,双指针法在数组,链表和字符串中很常用。
接着在字符串:替换空格,同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。
其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
那么针对数组删除操作的问题,其实在27. 移除元素中就已经提到了使用双指针法进行移除操作。
同样的道理在151.翻转字符串里的单词 中我们使用O(n)的时间复杂度,完成了删除冗余空格。
一些同学会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作,所以这也是典型的不知道库函数的时间复杂度,上来就用的案例了。
3.2 反转系列
在反转上还可以在加一些玩法,其实考察的是对代码的掌控能力。
541. 反转字符串II中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。
其实当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
因为要找的也就是每2 * k 区间的起点,这样写程序会高效很多。
在151.翻转字符串里的单词中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。
这道题目通过 先整体反转再局部反转,实现了反转字符串里的单词。
后来发现反转字符串还有一个牛逼的用处,就是达到左旋的效果。
在字符串:反转个字符串还有这个用处?中,我们通过先局部反转再整体反转达到了左旋的效果。
3.3 KMP
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
KMP的精髓所在就是前缀表,在KMP精讲中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。
前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
那么使用KMP可以解决两类经典问题:
- 匹配问题:28. 实现 strStr()
- 重复子串问题:459.重复的子字符串
再一次强调了什么是前缀,什么是后缀,什么又是最长相等前后缀。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
然后针对前缀表到底要不要减一,这其实是不同KMP实现的方式,我们在KMP精讲中针对之前两个问题,分别给出了两个不同版本的的KMP实现。
其中主要理解j=next[x]这一步最为关键!
4. 双指针篇
4.1 数组篇
在数组:就移除个元素很难么?中,原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。通过两个指针在一个for循环下完成两个for循环的工作
4.2 字符串篇
在字符串:这道题目,使用库函数一行代码搞定 中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。
使用双指针法,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。时间复杂度是O(n)。
在替换空格 中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了!
思路就是首先扩充数组到每个空格替换成"%20"之后的大小。然后双指针从后向前替换空格。
有同学问了,为什么要从后向前填充,从前向后填充不行么?
从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
其实很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
那么在字符串:花式反转还不够!中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要删除冗余空格。
在删除冗余空格的过程中,如果不注意代码效率,很容易写成了O(n^2)的时间复杂度。其实使用双指针法O(n)就可以搞定。
主要还是大家用erase用的比较随意,一定要注意for循环下用erase的情况,一般可以用双指针写效率更高!
4.3 链表篇
翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。
在链表:听说过两天反转链表又写不出来了?中,讲如何使用双指针法来翻转链表,只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。
思路还是很简单的,代码也不长,但是想在白纸上一次性写出bugfree的代码,并不是容易的事情。
在链表中求环,应该是双指针在链表里最经典的应用,在链表:环找到了,那入口呢?中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。
使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
那么找到环的入口,其实需要点简单的数学推理,我在文章中把找环的入口清清楚楚的推理的一遍,如果对找环入口不够清楚的同学建议自己看一看链表:环找到了,那入口呢?。
4.4 N数之和篇
在哈希表:解决了两数之和,那么能解决三数之和么?中,讲到使用哈希法可以解决1.两数之和的问题
其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了,大家可以尝试用双指针做一个leetcode上两数之和的题目,就可以体会到我说的意思了。
使用了哈希法解决了两数之和,但是哈希法并不使用于三数之和!
使用哈希法的过程中要把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是三数之和通过率如此之低的根源所在。
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。
只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。
在双指针法:一样的道理,能解决四数之和中,讲到了四数之和,其实思路是一样的,在三数之和的基础上再套一层for循环,依然是使用双指针法。
对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。
同样的道理,五数之和,n数之和都是在这个基础上累加。