438. 找到字符串中所有字母异位词
给定两个字符串 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 . l e n g t h , p . l e n g t h ≤ 3 ⋅ 1 0 4 1 \leq s.length, p.length \leq 3 \cdot 10^4 1≤s.length,p.length≤3⋅104
s
和p
仅包含小写字母
解法一(计数标识)
思路分析:
- 首先考虑特殊情况,当
s.length < p.length
时,s
不存在异位词子串,返回null
- 然后对字符串
p
中的字符进行计数,并拼接成一个字符串,即 异位词的 标识字符串。 - 再对字符串
s
,进行遍历,寻找符合条件的子串
实现代码如下:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int lenP = p.length(); // 字符串p的长度
int lenS = s.length(); // 字符串s的长度
List<Integer> ans = new ArrayList<>(); // 存储返回结果
// 存储标识
String flag = getFlagStr(p, 0, lenP);
// 遍历字符串s 寻找符合条件的异位词
for (int i = 0; i+lenP <= lenS; ++i) { // 遍历
String flagStr = getFlagStr(s, i, i+lenP);
if (flagStr.equals(flag)) {
ans.add(i);
}
}
return ans;
}
// 根据字符串中的字符及数量拼接成字符串 并返回
public String getFlagStr(String str, int start, int end) {
int[] count = new int[26];
for (int i = start; i < end; ++i) {
count[ str.charAt(i) - 'a' ] ++;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 26; ++i) {
if (count[i] > 0) {
sb.append(i + 'a').append(count[i]);
}
}
return new String(sb);
}
}
提交结果如下:
解答成功:
执行耗时:483 ms,击败了19.50% 的Java用户
内存消耗:43.3 MB,击败了41.64% 的Java用户
复杂度分析:
- 时间复杂度:
O
(
(
n
−
m
+
1
)
(
m
+
∣
Σ
∣
)
)
O((n-m+1)(m+|Σ|))
O((n−m+1)(m+∣Σ∣)),其中n指字符串
s
的长度,m指字符串p
的长度,不考虑equals()
方法的时间复杂度,且 ∣ Σ ∣ = 26 |Σ| = 26 ∣Σ∣=26,因为需要对26个字母进行计数 - 空间复杂度:
O
(
n
−
m
+
∣
Σ
∣
)
O(n-m+|Σ|)
O(n−m+∣Σ∣),其中返回结果最多存储
n-m
,此外还有计数数组,对于调用函数的开销不计算在内。
解法二(滑动窗口+计数)
思路分析:
- 建立两个数组,并分别用来实时统计字符串s和p的字符,然后通过滑动窗口来改变统计字符串s的字符数量,如此减少了对一些字符的重复统计
- 确认窗口长度为 p 字符串长度,然后sCount数组则为窗口内字符的统计
- 每次将窗口向前移动一步,并动态改变窗口内的字符统计数组sCount
- 移动窗口后,与异位词的pCount统计数组进行比较,为异位词则将窗口起始位置保存
实现代码如下:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int lenP = p.length(); // 字符串p的长度
int lenS = s.length(); // 字符串s的长度
List<Integer> ans = new ArrayList<>(); // 存储返回结果
if (lenS < lenP) // 若字符串s的长度小于字符串p 则直接返回空列表
return ans;
int[] sCount = new int[26]; // 统计s中字符的计数数组
int[] pCount = new int[26]; // 统计p中字符的计数数组
// 统计p字符串的字符
for (int i = 0; i < lenP; ++i) {
++ pCount[p.charAt(i) - 'a'];
++ sCount[s.charAt(i) - 'a']; // 先统计s中起始索引为0长度为lenP的子串
}
if (Arrays.equals(sCount, pCount)) {
ans.add(0); // 若子串符合要求 则存储其起始索引
}
for (int i = 0; i+lenP < lenS; ++i) {
-- sCount[s.charAt(i) - 'a']; // 排除左窗口起始位置的字符
++ sCount[s.charAt(i+lenP) - 'a']; // 新增右窗口处的字符
if (Arrays.equals(sCount, pCount)) {
ans.add(i+1); // 窗口内子串 为异位词 则将起始位置保存
}
}
return ans;
}
}
提交结果如下:
解答成功:
执行耗时:7 ms,击败了90.96% 的Java用户
内存消耗:42.5 MB,击败了91.01% 的Java用户
复杂度分析:
- 时间复杂度: O ( m + ( n − m ) × ∣ Σ ∣ ) O(m+(n-m)\times∣Σ∣) O(m+(n−m)×∣Σ∣),m为字符串p的长度,n为字符串s的长度,∣Σ∣为统计数组的长度,每次判断s的字串是否与p为异位词,需要花费 O ( ∣ Σ ∣ ) O(∣Σ∣) O(∣Σ∣)
- 空间复杂度: O ( ∣ Σ ∣ ) O(∣Σ∣) O(∣Σ∣),不包括返回结果的存储,使用了两个统计字符的数组来存储字符数目
解法三(滑动窗口优化)
思路分析:
- 当两个字符串包含的字符及数目相同时,即符合异位词,因此对于s中的子串与字符串p的字符差异个数,可以使用一个变量distance来标记
- 当distance为0时,则说明此时符合异位词,并将窗口的起始索引保存到返回值中。
实现代码如下:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int lenP = p.length(); // 字符串p的长度
int lenS = s.length(); // 字符串s的长度
List<Integer> ans = new ArrayList<>(); // 存储返回结果
if (lenS < lenP) // 若字符串s的长度小于字符串p 则直接返回空列表
return ans;
int[] count = new int[26]; // 用于统计字符数
int distance = 0; // 用于计数 s子串与字符串p的字符差异个数
for (int i = 0; i < lenP; ++i) {
-- count[p.charAt(i) - 'a']; // p 字符数减少
++ count[s.charAt(i) - 'a']; // s 字符数增加
}
for (int number : count) { // 判断s中[0, lenP)的字串是否为 异位词
if (number != 0) {
++ distance;
}
}
if (distance == 0) { // 说明s中[0,lenP)的子串为异位词 将其起始索引保存
ans.add(0);
}
for (int i = 0; i+lenP < lenS; ++i) {
// 排除左边界字符
int chL = s.charAt(i) - 'a';
-- count[chL];
if (count[chL] == -1) { // 说明子串中该字符数目 由符合变不符合
++ distance; // 即不符合条件的字符数+1
} else if (count[chL] == 0) { // 说明子串中该字符数 由不符合变符合
-- distance; // 即符合条件的字符数-1
}
// 添加右边界字符
int chR = s.charAt(i+lenP) - 'a';
++ count[chR];
if (count[chR] == 1) { // 说明子串中该字符数目 由符合变不符合
++ distance; // 即不符合条件的字符数+1
} else if (count[chR] == 0) { // 说明子串中该字符数 由不符合变符合
-- distance; // 即不符合条件的字符数-1
}
// 判断新的子串是否 符合异位词
if (distance == 0) {
ans.add(i+1);
}
}
return ans;
}
}
提交结果如下:
解答成功:
执行耗时:6 ms,击败了93.93% 的Java用户
内存消耗:42.7 MB,击败了66.14% 的Java用户
复杂度分析:
- 时间复杂度:
O
(
m
+
n
+
Σ
)
O(m+n+Σ)
O(m+n+Σ),n为字符串s的长度,m为字符串p的长度,
O
(
Σ
)
O(Σ)
O(Σ)为初始化
distance
的时间复杂度 - 空间复杂度: O ( Σ ) O(Σ) O(Σ)。用于存储滑动窗口和字符串 p 中每种字母数量的差。