LeetCode第438题:找到字符串中所有的异位词。
题目:给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
这道题对我来说上了难度,昨晚光看答案都脑补了一晚上,今晚来复盘一下
案例: s = "cbaebabacd", p = "abc" , 返回[0,6]。 因为s中符合要求的切片是"cba"和"bac",他们在s数组中对应的首字母序号分别是0:s.charAt(0)=a和6:s.charAt(6)=b。
比较容易想到的思路:由于p的长度为3,那么从字符串s的第一个字符开始,截取长度为3的子串,直到s的倒数第三个字符。每次取到子串后,将子串转为数组排序,然后再将字符串p转为数组后排序,
将两个数组进行比较,如果结果一致,就说明该s的子串与p是异位词,此时该子串首字符在s中的序号就为一个答案。
从s的第一个字符开始一直到倒数第三个字符,能截取到的子串为“cba”、“bae”、“aeb”、“eba”......“bac”、“acd”,其中“cba”和“bac”子串满足“abc”的异位词。
我感觉这种思路比较简单粗暴,应该不是题目预期的解答。用这种方法提交会导致时间复杂度过高无法通过字符串很长很长的案例,瞟了一眼答案,果然没有这么容易,原来这道题需要使用滑窗算法求解。
跟着答案学习滑窗算法思路:
1、获取字符串p中每个字母转换为数字后的值,进而把每个值出现的次数记录在一个新的数组pCount中。新的数组初始是一个包含26个0的数组,即int[] pCount = [0]*26。将字母转换为数字可使用 p.charAt[i]-'a',
如果i=2,那么 p.charAt[i]-'a' = p.charAt(3)-'a' = 'c'-'a' = 2。 c只在p(abc)中出现了一次,所以将pCount中下表为2的元素++1 ,即p[2] = 1。 由于需要对p所有元素,即a、b、c都执行该操作,
因此最终pCount=[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],代表p中a,b,c各出现一次。
2、对字符串s的前p.length个元素执行同样的操作,在这个案例中,统计字符串s中前三个字母转换为数字后的值出现的次数记录在一个新的数组sCount中,由于字符串s的前三个字母也是abc,
因此最终sCount=[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],代表s中a,b,c各出现一次。
这时我们比较pCount和sCount,发现两个数组相等,说明字符串s的前三个字母abc各出现了一次,与字符串p一致,所以s的前三个字母和p互为异位词,s[0]可以作为异位词的开头,所以0为一个答案。
先将s的长度和p的长度分别定义为sLen = s.length,pLen=p.length
返回的答案为一个名为answer的列表:
int sLen =s.length(), pLen = p.length();
List<Integer> answer = new ArrayList<Integer>();
复制
步骤1和2的代码实现:
int[] sCount = new int[26];
int[] pCount = new int[26];
for (int i = 0; i < pLen; i++) {
sCount[s.charAt(i) - 'a']++;
pCount[p.charAt(i) - 'a']++;
}
if(Arrays.equals(sCount,pCount)){
answer.add(0);
}
复制
3、此后我们从s的第2个字母到第4个字母组成的字符串开始与p进行比较,也就是把“c[bae]babacd”(加粗部分)与“abc”进行比较,比较的方法和步骤2一致。
但是不一样的是,我们在比较前,需要移除掉上一次比较的字符串首字母的计数,然后增加本次比较字符串最后一个字母的计数。
之后,我们可以比较pCount和
第3个字母到第5个字母组成的字符串对应的sCount,
第4个字母到第6个字母组成的字符串对应的sCount,
第5个字母到第7个字母组成的字符串对应的sCount,
第6个字母到第8个字母组成的字符串对应的sCount,
第7个字母到第9个字母组成的字符串对应的sCount,
第8个字母到第10个字母组成的字符串对应的sCount,
最大只能取到s的第8个字母了,如果取第9个字母那么字符串s还剩下的字符数是无法达到字符串p的长度的,所以在for循环的时候范围是for(int i=0;i<sLen - pLen;i++)
举个例子:
当完成步骤2后,sCount=[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],这是我们比较了字符串s中abc的结果,
而现在我们需要的是字符串s中的bae在sCount中的表示,其中ba已经计算过了,e还没有计算过,只需要把e出现的这一次加上去,预期返回的sCount是[1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]。
为了返回这个结果,我们要删掉上次比较中计算“c”出现一次的记录,即让sCount['c'-'a']的值从1减掉1变为0,否则在完成第二轮比较后会返回[1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],这样显然是错误的。
步骤3代码实现:
// 从s数组的第pLen+1个数开始比较
int gap = sLen - pLen;
for(int i=0;i<gap;i++)
{
// s数组第从第0个下标开始遍历的第i个字母转换成数字的值
int j = s.charAt(i) - 'a';
// s数组第从第pLen个下标开始遍历的第i个字母转换成数字的值
int k = s.charAt(i+pLen) - 'a';
// 去掉上一次遍历后用于比较的数组中第一位字母的计数,
// 增加这次遍历后用于比较的数组中最后一位字母的计数
sCount[j]--;
sCount[k]++;
if(Arrays.equals(sCount,pCount))
{
answer.add(i+1);
}
}
复制
4、特殊情况下,如果字符串p的长度>字符串s的长度,那么是没有比较的意义的,所以直接返回一个空的答案。
if (sLen < pLen) {
return answer;
}
复制
这样就大功告成了。
题目完整代码:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen =s.length(), pLen = p.length();
List<Integer> answer = new ArrayList<Integer>();
if (sLen < pLen) {
return answer;
}
int[] sCount = new int[26];
int[] pCount = new int[26];
for (int i = 0; i < pLen; i++) {
sCount[s.charAt(i) - 'a']++;
pCount[p.charAt(i) - 'a']++;
}
System.out.println("打印比较s中前pLen个字母是否与p为异位词");
// System.out.println(Arrays.toString(sCount));
// System.out.println(Arrays.toString(pCount));
if(Arrays.equals(sCount,pCount)){
answer.add(0);
}
// 从s数组的第pLen+1个数开始比较
int gap = sLen - pLen;
for(int i=0;i<gap;i++)
{
// s数组第从第0个下标开始遍历的第i个字母转换成数字的值
int j = s.charAt(i) - 'a';
// s数组第从第pLen个下标开始遍历的第i个字母转换成数字的值
int k = s.charAt(i+pLen) - 'a';
// 去掉上一次遍历后用于比较的数组中第一位字母的计数,
// 增加这次遍历后用于比较的数组中最后一位字母的计数
sCount[j]--;
sCount[k]++;
if(Arrays.equals(sCount,pCount))
{
answer.add(i+1);
}
}
return answer;
}
}
复制
拿着官方答案的写法取提交,自然是通过的。。。
其实官方还提供了一个更优化效率的办法,但是把第一种方法搞明白已经让我的脑细胞不够了,不打算继续看了。。。