1. 题目
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/fi…
2. 示例
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
3. 题解
3.1 暴力方法
由于题目所出现的字符都是小写字母,而题目中的异位词指的是字符相同但是顺序不同的字符串,这时我们可以想到使用一个长度为26(26个小写字母)的数组作为字符串的key,这样一来,只要key相同的字符串它们就互为异位词。
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length();
int pLen = p.length();
String pKey = getKey(p);
List<Integer> res = new ArrayList<>();
for (int i = 0; i <= sLen - pLen; i++) {
String sub = s.substring(i, i + pLen);
// O(pLen)
String key = getKey(sub);
// O(pLen)
if (key.equals(pKey)) {
res.add(i);
}
}
return res;
}
// O(pLen)
private String getKey(String str) {
int[] count = new int[26];
for (int i = 0; i < str.length(); i++) {
count[str.charAt(i) - 'a']++;
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < count.length; i++) {
if (count[i] != 0) {
builder.append((char) (i + 'a')).append(count[i]);
}
}
return builder.toString();
}
3.2 滑动窗口
上述将字符串生成的数组转换成key(也是字符串),再进行比较,效率十分低下。因此,我们可以考虑使用滑动窗口。
这样一来,我们只需要维护窗口内出现的字符是字符串p中的字符并且各个字符的数量也需要和p中一致。
但是,如何维护窗口内字符和字符数量同时一致呢?
这时,我们可以沿用上述暴力方法中的长度为26的数组来维护我们当前遍历到的窗口内的字符串。也使用索引值表示字符,数组索引处的值来表示该字符出现的次数。初始化时,根据字符串p生成数组原始值。之后开始遍历字符串s,当字符进入窗口时则数组对应位置值-1,当弹出窗口时则数组对应位置值 +1(还原)。
1. 第一版的滑动窗口
// 滑动窗口
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length();
int pLen = p.length();
List<Integer> res = new ArrayList<>();
int[] pCounts = new int[26];
for (int i = 0; i < pLen; i++) {
pCounts[p.charAt(i) - 'a']++;
}
int left = 0;
int right = 0;
while (right < sLen) {
// 加入窗口的字符的索引
int inWin = s.charAt(right) - 'a';
// 这里直接将遍历到的(右边界)字符加入到窗口中
pCounts[inWin]--;
while (pCounts[inWin] < 0) {
int outWin = s.charAt(left++) - 'a';
pCounts[outWin]++;
}
if (right - left + 1 == pLen) {
res.add(left);
}
right++;
}
return res;
}
2. 第二版的滑动窗口
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length();
int pLen = p.length();
List<Integer> res = new ArrayList<>();
int[] pCounts = new int[26];
for (int i = 0; i < pLen; i++) {
pCounts[p.charAt(i) - 'a']++;
}
int left = 0;
int right = 0;
while (right < sLen) {
// 准备加入窗口的字符的索引
int inWin = s.charAt(right) - 'a';
// 这里先试探性的查看加入的字符是否影响窗口内字符的正确性
while (pCounts[inWin] - 1 < 0) {
int outWin = s.charAt(left++) - 'a';
pCounts[outWin]++;
}
// 在处理完成后,再加入对应的字符
pCounts[inWin]--;
if (right - left + 1 == pLen) {
res.add(left);
}
right++;
}
return res;
}
上述两种代码其主体思路是一样的,唯一不同点在于当前遍历到的入窗字符(即右边界)是先直接加入窗口进行处理还是处理后再加入。
使用滑动窗口有时还会用到哈希表(这里主要指的是Set),如求一个给定字符串中最长的字符全部相异的长度。这种情况下,在遍历右边界时就不能先直接加入窗口后处理,而是试探性的检查当前入窗字符的正确性并处理后再加入,这是因为哈希表中的每个元素具有唯一性。