题目描述
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104
s
和p
仅包含小写字母
题解思路
方法一:哈希表
时空复杂度: O(m + (n−m) × (Σ + m)) 这里的两个字符串长度决定了循环的次数; 空间复杂度为O(26)即26个英文字母的大小
我们可以利用哈希表存储p字符串的所有字母出现的个数,然后循环遍历s字符串每个字母,然后截取区间内的字符出现的次数,和哈希表存储的个数进行比较,当相等的时候就可以得到起始索引,当然,这种方法应该算是比较暴力的方法了。
上代码:
public List<Integer> hashMethod(String s,String p) {
if(s.length() < p.length()) {
return new ArrayList<Integer>();
}
//得到p的计数数组
int[] countp = new int[26];
//遍历p得到计数
for(int i = 0; i < p.length(); i ++) {
countp[p.charAt(i) - 'a'] ++;
}
//创建结果List
List<Integer> list = new ArrayList();
//遍历s字符串
for(int i = 0; i <= s.length() - p.length(); i ++) {
//获取计数数组
int[] counts = new int[26];
for(int j = i; j < i + p.length(); j ++) {
counts[s.charAt(j) - 'a'] ++;
}
//当counts和countp相等的时候就加入list中
if(Arrays.equals(counts,countp)) {
list.add(i);
}
}
return list;
}
方法二:滑动窗口
时空复杂度: O(m+(n−m)×Σ) 这里的时间复杂度取决于s长度的大小,空间复杂度为 O(Σ) 只需要维护一个26字母所以这里 Σ=26
我们利用两个字符串的每个字符个个数作为滑动窗口的判断条件,定义两个指针,一个指向左边,一个指向右边,两个指针中间维护的窗口中的字符个数应该和p字符串的中字符种类和个数相同,当右指针往右移动的时候判断是否满足条件,当满足条件就记录起始点。
public List<Integer> slideMethod(String s,String p) {
//首先判断s和t的大小
if(s.length() < p.length()) {
return new ArrayList<Integer>();
}
//创建结果集合
List<Integer> res = new ArrayList();
//创建p字符串的计数数组
int[] countp = new int[26];
for(int i = 0; i < p.length(); i ++) {
countp[p.charAt(i) - 'a'] ++;
}
//创建左右指针,维护滑动窗口
int left = 0;
int[] counts = new int[26];
for(int right = 0; right < s.length(); right ++) {
//将right指针指向的位置的字符加入count中
counts[s.charAt(right) - 'a'] ++;
//当区间长度大于p的长度就让left指针右移
if(right - left + 1 > p.length()) {
counts[s.charAt(left) - 'a'] --;
left ++;
}
//判断当前区间是否符合条件
if(Arrays.equals(countp,counts)) {
res.add(left);
}
}
return res;
}
方法三:滑动窗口(优化版)
时空复杂度: O(n+m+Σ) O(Σ)
根据上一个普通的滑动窗口方法,我们可以做出改进,取消使用两个count计数数组,而是改为使用diff变量记录两个数组中每个数字出现次数的差异。当diff = 0的时候代表滑动窗口内的元素是异位数。下面我们用代码来实现这个思想
public List<Integer> slideOpt(String s,String p) {
//先判断两个字符串的大小
if(s.length() < p.length()) return new ArrayList();
//创建一个count数组用于记录每个元素出现的次数
int[] count = new int[26];
//创建diff变量
int diff = 0;
for(int i = 0; i < p.length(); i ++) {
count[s.charAt(i) - 'a'] ++;
count[p.charAt(i) - 'a'] --;
}
//判断出现元素的差异个数
for(int i = 0; i < 26; i ++) {
if(count[i] != 0) {
diff ++;
}
}
//创建结果集合
List<Integer> res = new ArrayList();
if(diff == 0) {
res.add(0);
}
//这里是滑动窗口移动的距离,从0一直移动到 s.len - p.len - 1
for(int i = 0; i < s.length() - p.length(); i ++) {
//向右滑动判断diff的改变
if(count[s.charAt(i) - 'a'] == 1) { //等于1代表没移动之前窗口中比p中多一个元素,移动后可以抹去一个差异
diff --;
} else if(count[s.charAt(i) - 'a'] == 0) { //等于0的话,移动后就多了一个差异
diff ++;
}
//减去左边移去的元素
count[s.charAt(i) - 'a'] --;
//判断右端点diff后的改变
if(count[s.charAt(i + p.length()) - 'a'] == -1) { //这里为-1代表没移动之前比p中少一个元素,移动后可以抹去差异
diff --;
} else if(count[s.charAt(i + p.length()) - 'a'] == 0) {
diff ++;
}
//加入右边移动的元素
count[s.charAt(i + p.length()) - 'a'] ++;
//判断diff是不是为0
if(diff == 0) {
res.add(i + 1); //注意:这里是加入移动后的位置即i+1
}
}
return res;
}
总结
这题使用基础暴力方法,选择对了数据结构也是可以做出来的,就是时间复杂度可能比较高,但还是可以通过 leetcode 的数据测试,想要优化的话,我们就需要使用滑动窗口算法,这种方法是用于在一个字符串中查找子串,或者判断异位词等多种情况下大幅度减小时间复杂度的一种方法。需要我们多积累滑动窗口的应用场景!