文章目录
题目
标题和出处
标题:字符串的排列
出处:567. 字符串的排列
难度
4 级
题目描述
要求
给定两个字符串 s1 \texttt{s1} s1 和 s2 \texttt{s2} s2,如果 s2 \texttt{s2} s2 包含 s1 \texttt{s1} s1 的一个排列则返回 true \texttt{true} true,否则返回 false \texttt{false} false。
换言之,如果 s1 \texttt{s1} s1 的排列之一是 s2 \texttt{s2} s2 的子串则返回 true \texttt{true} true。
示例
示例 1:
输入: s1 = "ab", s2 = "eidbaooo" \texttt{s1 = "ab", s2 = "eidbaooo"} s1 = "ab", s2 = "eidbaooo"
输出: true \texttt{true} true
解释: s2 \texttt{s2} s2 包含 s1 \texttt{s1} s1 的排列之一( "ba" \texttt{"ba"} "ba")。
示例 2:
输入: s1 = "ab", s2 = "eidboaoo" \texttt{s1 = "ab", s2 = "eidboaoo"} s1 = "ab", s2 = "eidboaoo"
输出: false \texttt{false} false
数据范围
- 1 ≤ s1.length, s2.length ≤ 10 4 \texttt{1} \le \texttt{s1.length, s2.length} \le \texttt{10}^\texttt{4} 1≤s1.length, s2.length≤104
- s1 \texttt{s1} s1 和 s2 \texttt{s2} s2 仅包含小写英语字母
解法一
思路和算法
如果字符串 s 2 s_2 s2 包含字符串 s 1 s_1 s1 的一个排列,则 s 1 s_1 s1 的长度必须小于或等于 s 2 s_2 s2 的长度。如果 s 1 s_1 s1 的长度大于 s 2 s_2 s2 的长度,则 s 2 s_2 s2 一定不包含 s 1 s_1 s1 的排列,返回 false \text{false} false。
当 s 1 s_1 s1 的长度小于或等于 s 2 s_2 s2 的长度时,需要遍历 s 2 s_2 s2 的每个长度为 s 1 s_1 s1 长度的子字符串,判断是否存在子字符串是 s 1 s_1 s1 的一个排列。字符串 s 1 s_1 s1 的任意排列都满足排列中的每个字符的出现次数与 s 1 s_1 s1 中的每个字符的出现次数相同,因此可以比较每个字符的出现次数,判断每个子字符串是否为 s 1 s_1 s1 的排列。以下所说的 s 2 s_2 s2 的子字符串均表示长度为 s 1 s_1 s1 长度的子字符串。
首先遍历 s 1 s_1 s1 计算每个字符的出现次数,然后遍历 s 2 s_2 s2 的首个子字符串并计算每个字符的出现次数。如果 s 2 s_2 s2 的首个子字符串中的每个字符的出现次数与 s 1 s_1 s1 中的每个字符的出现次数相同,则 s 2 s_2 s2 的首个子字符串即为 s 1 s_1 s1 的排列,返回 true \text{true} true。
如果 s 2 s_2 s2 的首个子字符串不是 s 1 s_1 s1 的排列,则需要继续遍历其余的子字符串并判断是否为 s 1 s_1 s1 的排列。每次将子字符串的下标范围向右移动一位,则有一个字符移出子字符串,有一个字符移入子字符串,更新子字符串中这两个字符的出现次数之后,比较子字符串中的每个字符的出现次数是否与 s 1 s_1 s1 中的每个字符的出现次数相同。如果遇到一个子字符串的每个字符的出现次数与 s 1 s_1 s1 中的每个字符的出现次数相同,则该子字符串即为 s 1 s_1 s1 的排列,返回 true \text{true} true。
如果遍历结束之后没有在 s 2 s_2 s2 中找到 s 1 s_1 s1 的排列,返回 false \text{false} false。
代码
java
class Solution {
public boolean checkInclusion(String s1, String s2) {
int length1 = s1.length(), length2 = s2.length();
if (length1 > length2) {
return false;
}
int[] counts1 = new int[26];
int[] counts2 = new int[26];
for (int i = 0; i < length1; i++) {
char c1 = s1.charAt(i);
counts1[c1 - 'a']++;
char c2 = s2.charAt(i);
counts2[c2 - 'a']++;
}
if (checkEqual(counts1, counts2)) {
return true;
}
for (int i = length1; i < length2; i++) {
char prev = s2.charAt(i - length1);
counts2[prev - 'a']--;
char curr = s2.charAt(i);
counts2[curr - 'a']++;
if (checkEqual(counts1, counts2)) {
return true;
}
}
return false;
}
public boolean checkEqual(int[] counts1, int[] counts2) {
for (int i = 0; i < 26; i++) {
if (counts1[i] != counts2[i]) {
return false;
}
}
return true;
}
}
复杂度分析
-
时间复杂度: O ( ( m + n ) × ∣ Σ ∣ ) O((m + n) \times |\Sigma|) O((m+n)×∣Σ∣),其中 m m m 和 n n n 分别是字符串 s 1 s_1 s1 和 s 2 s_2 s2 的长度, Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。只有当 s 1 s_1 s1 的长度小于等于 s 2 s_2 s2 的长度时才需要遍历字符串 s 2 s_2 s2 寻找 s 1 s_1 s1 的排列,需要遍历字符串 s 1 s_1 s1 和 s 2 s_2 s2 各一次,对于每个子字符串需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的时间判断是否为 s 1 s_1 s1 的排列。
-
空间复杂度: O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣),其中 Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。记录 s 1 s_1 s1 和 s 2 s_2 s2 中每个字母的出现次数需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的空间。
解法二
思路和算法
解法一对于 s 2 s_2 s2 的每个子字符串都需要遍历所有字母的计数。由于每次将子字符串的下标范围向右移动一位时,最多只会有两个字符的出现次数有变化,因此可以将每个子字符串的判断时间降低到 O ( 1 ) O(1) O(1)。
同时遍历 s 1 s_1 s1 以及 s 2 s_2 s2 的首个子字符串,计算每个字符在 s 2 s_2 s2 的子字符串与 s 1 s_1 s1 中的出现次数之差,并记录出现次数之差不为 0 0 0 的字符个数。如果每个字符在 s 2 s_2 s2 的首个子字符串与 s 1 s_1 s1 中的出现次数之差都是 0 0 0,则 s 2 s_2 s2 的首个子字符串即为 s 1 s_1 s1 的排列,返回 true \text{true} true。
如果 s 2 s_2 s2 的首个子字符串不是 s 1 s_1 s1 的排列,则需要继续遍历其余的子字符串并判断是否为 s 1 s_1 s1 的排列。每次将子字符串的下标范围向右移动一位,则有一个字符移出子字符串,有一个字符移入子字符串,将移出子字符串的字符记为 prev \textit{prev} prev,将移入子字符串的字符记为 curr \textit{curr} curr。如果 prev = curr \textit{prev} = \textit{curr} prev=curr,则子字符串中的每个字符的出现次数没有发生变化,因此不执行任何操作。如果 prev ≠ curr \textit{prev} \ne \textit{curr} prev=curr,则将 prev \textit{prev} prev 的出现次数之差减 1 1 1,将 curr \textit{curr} curr 的出现次数之差加 1 1 1,更新字符的出现次数之差之后,更新出现次数之差不为 0 0 0 的字符个数,更新方法如下。
-
如果 prev \textit{prev} prev 的出现次数之差为 − 1 -1 −1,则将出现次数之差不为 0 0 0 的字符个数加 1 1 1;如果 prev \textit{prev} prev 的出现次数之差为 0 0 0,则将出现次数之差不为 0 0 0 的字符个数减 1 1 1。
-
如果 curr \textit{curr} curr 的出现次数之差为 1 1 1,则将出现次数之差不为 0 0 0 的字符个数加 1 1 1;如果 curr \textit{curr} curr 的出现次数之差为 0 0 0,则将出现次数之差不为 0 0 0 的字符个数减 1 1 1。
更新字符的出现次数之差以及出现次数之差不为 0 0 0 的字符个数之后,如果出现次数之差不为 0 0 0 的字符个数为 0 0 0,则当前子字符串为 s 1 s_1 s1 的排列,返回 true \text{true} true。
如果遍历结束之后没有在 s 2 s_2 s2 中找到 s 1 s_1 s1 的排列,返回 false \text{false} false。
代码
java
class Solution {
public boolean checkInclusion(String s1, String s2) {
int length1 = s1.length(), length2 = s2.length();
if (length1 > length2) {
return false;
}
int[] counts = new int[26];
for (int i = 0; i < length1; i++) {
char c1 = s1.charAt(i), c2 = s2.charAt(i);
if (c1 == c2) {
continue;
}
counts[c1 - 'a']--;
counts[c2 - 'a']++;
}
int diffs = 0;
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
diffs++;
}
}
if (diffs == 0) {
return true;
}
for (int i = length1; i < length2; i++) {
char prev = s2.charAt(i - length1), curr = s2.charAt(i);
if (prev == curr) {
continue;
}
counts[prev - 'a']--;
counts[curr - 'a']++;
if (counts[prev - 'a'] == -1) {
diffs++;
} else if (counts[prev - 'a'] == 0) {
diffs--;
}
if (counts[curr - 'a'] == 1) {
diffs++;
} else if (counts[curr - 'a'] == 0) {
diffs--;
}
if (diffs == 0) {
return true;
}
}
return false;
}
}
复杂度分析
-
时间复杂度: O ( m + n + ∣ Σ ∣ ) O(m + n + |\Sigma|) O(m+n+∣Σ∣),其中 m m m 和 n n n 分别是字符串 s 1 s_1 s1 和 s 2 s_2 s2 的长度, Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。只有当 s 1 s_1 s1 的长度小于等于 s 2 s_2 s2 的长度时才需要遍历字符串 s 2 s_2 s2 寻找 s 1 s_1 s1 的排列,需要遍历字符串 s 1 s_1 s1 和 s 2 s_2 s2 各一次,对于 s 2 s_2 s2 的首个子字符串需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的时间判断是否为 s 1 s_1 s1 的排列,对于 s 2 s_2 s2 的其余每个子字符串需要 O ( 1 ) O(1) O(1) 的时间判断是否为 s 1 s_1 s1 的排列。
-
空间复杂度: O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣),其中 Σ \Sigma Σ 是字符集,这道题中 Σ \Sigma Σ 是全部小写英语字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。记录 s 1 s_1 s1 和 s 2 s_2 s2 中每个字母的出现次数需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣) 的空间。