题目介绍
力扣438题:https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。
- 不考虑答案输出的顺序。
分析
“字母异位词”,指“字母相同,但排列不同的字符串”。注意这里所说的“排列不同”,是所有字母异位词彼此之间而言的,并不是说要和目标字符串p不同。另外,我们同样应该考虑,p中可能有重复字母。
方法一:暴力法
最简单的想法,自然还是暴力法。就是直接遍历s中每一个字符,把它当作子串的起始,判断长度为p.length()的子串是否是p的字母异位词就可以了。
考虑到子串和p中都可能有重复字母,我们还是用一个额外的数据结构,来保存每个字母的出现频次。由于本题的字符串限定只包含小写字母,所以我们可以简单地用一个长度为26的int类型数组来表示,每个位置存放的分别是字母a~z的出现个数。
代码演示如下:
// 方法一:暴力法,枚举所有的长度为p.length()的子串
public List<Integer> findAnagrams1(String s, String p){
// 定义一个结果列表
ArrayList<Integer> result = new ArrayList<>();
// 1. 统计p中所有字符频次
int[] pCharCounts = new int[26];
for (int i = 0; i < p.length(); i++){
pCharCounts[p.charAt(i) - 'a'] ++;
}
// 2. 遍历s,以每一个字符作为起始,考察长度为p.length()的子串
for (int i = 0; i <= s.length() - p.length(); i++){
// 3. 判断当前子串是否为p的字母异位词
boolean isMatched = true;
// 定义一个数组,统计子串中所有字符频次
int[] subStrCharCounts = new int[26];
for (int j = i; j < i + p.length(); j++){
subStrCharCounts[s.charAt(j) - 'a'] ++;
// 判断当前字符频次,如果超过了p中的频次,就一定不符合要求
if (subStrCharCounts[s.charAt(j) - 'a'] > pCharCounts[s.charAt(j) - 'a']){
isMatched = false;
break;
}
}
if (isMatched)
result.add(i);
}
return result;
}
复杂度分析
- 时间复杂度:O(|s| * |p|),其中|s|表示s的长度,|p|表示p的长度。时间开销主要来自双层循环,循环的迭代次数分别是(s.length-p.length+1)和
p.length, 所以时间复杂度为O((|s|-|p|+1) * |p|), 去除低阶复杂度,最终的算法复杂度为 O(|s| * |p|)。 - 空间复杂度:O(1)。需要两个大小为 26的计数数组,分别保存p和当前子串的字母个数。尽管循环迭代过程中在不断申请新的空间,但是上一次申请的数组空间应该可以得到复用,所以实际上一共花费了2个数组的空间,因为数组大小是常数,所以空间复杂度为O(1)。
方法二:滑动窗口(双指针)
暴力法的缺点是显而易见的:时间复杂度较大,运行耗时较长。
我们在暴力求解的时候,其实对于很多字母是做了多次统计的。子串可以看作字符串上开窗截取的结果,自然想到,可以定义左右指针向右移动,实现滑动窗口的作用。在指针移动的过程中,字符只会被遍历一次,时间复杂度就可以大大降低。
代码演示如下:
// 方法二:滑动窗口法,分别移动起始和结束位置
public List<Integer> findAnagrams(String s, String p){
// 定义一个结果列表
ArrayList<Integer> result = new ArrayList<>();
// 1. 统计p中所有字符频次
int[] pCharCounts = new int[26];
for (int i = 0; i < p.length(); i++){
pCharCounts[p.charAt(i) - 'a'] ++;
}
// 统计子串中所有字符出现频次的数组
int[] subStrCharCounts = new int[26];
// 定义双指针,指向窗口的起始和结束位置
int start = 0, end = 1;
// 2. 移动指针,总是截取字符出现频次全部小于等于p中字符频次的子串
while (end <= s.length()){
// 当前新增字符
char newChar = s.charAt(end - 1);
subStrCharCounts[newChar - 'a'] ++;
// 3. 判断当前子串是否符合要求
// 如果新增字符导致子串中频次超出了p中频次,那么移动start,消除新增字符的影响
while ( subStrCharCounts[newChar - 'a'] > pCharCounts[newChar - 'a'] && start < end){
char removedChar = s.charAt(start);
subStrCharCounts[removedChar - 'a'] --;
start ++;
}
// 如果当前子串长度等于p的长度,那么就是一个字母异位词
if (end - start == p.length())
result.add(start);
end ++;
}
return result;
}
复杂度分析
- 时间复杂度:O(|s|)。 窗口的左右指针最多都到达 s 串结尾,s 串每个字符最多被左右指针都经过一次,所以时间复杂度为O(|s|)。
- 空间复杂度:O(1)。只需要两个大小为 26 的计数数组,大小均是确定的常量,所以空间复杂度为O(1)。