题目
给你两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。
换句话说,s1
的排列之一是 s2
的 子串 。
示例一:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例二:
输入:s1= "ab" s2 = "eidboaoo"
输出:false
方法一:滑动窗口
思想
由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。
根据这一性质,记 s1 的长度为 n,我们可以遍历 s2中的每个长度为 n 的子串,判断子串和 s1 中每个字符的个数是否相等,若相等则说明该子串是 s1 的一个排列。
使用两个数组 cnt1 和 cnt2,cnt1\textit{cnt}_1cnt1 统计 s1中各个字符的个数,cnt2 统计当前遍历的子串中各个字符的个数。
由于需要遍历的子串长度均为 n,我们可以使用一个固定长度为 n 的滑动窗口来维护 cnt2:滑动窗口每向右滑动一次,就多统计一次进入窗口的字符,少统计一次离开窗口的字符。然后,判断 cnt1 是否与 cnt2相等,若相等则意味着 s1s_1s1 的排列之一是 s2 的子串。
JAVA代码
class Solution {
public boolean checkInclusion(String s1, String s2) {
int n = s1.length(), m = s2.length();
if (n > m) {
return false;
}
int[] cnt1 = new int[26];
int[] cnt2 = new int[26];
for (int i = 0; i < n; ++i) {
++cnt1[s1.charAt(i) - 'a'];
++cnt2[s2.charAt(i) - 'a'];
}
if (Arrays.equals(cnt1, cnt2)) {
return true;
}
for (int i = n; i < m; ++i) {
++cnt2[s2.charAt(i) - 'a'];
--cnt2[s2.charAt(i - n) - 'a'];
if (Arrays.equals(cnt1, cnt2)) {
return true;
}
}
return false;
}
}
方法二:优化滑动窗口
优化思想
注意到每次窗口滑动时,只统计了一进一出两个字符,却比较了整个 cnt1 和 cnt2 数组。
从这个角度出发,我们可以用一个变量 diff 来记录 cnt1 与 cnt2 的不同值的个数,这样判断 cnt1和 cnt2 是否相等就转换成了判断 diff 是否为 0.
每次窗口滑动,记一进一出两个字符为 x 和 y。
若 x=y 则对 cnt2 无影响,可以直接跳过。
若 x≠y,对于字符 x,在修改 cnt2 之前若有 cnt2[x]=cnt1[x],则将 diff 加一;在修改 cnt2\textit{cnt}_2cnt2 之后若有 cnt2[x]=cnt1[x],则将 diff 减一。字符 y 同理。
此外,为简化上述逻辑,我们可以只用一个数组 cnt,其中 cnt[x]=cnt2[x]−cnt1[x],将 cnt1[x] 与 cnt2[x] 的比较替换成 cnt[x] 与 0 的比较。
JAVA代码
class Solution {
public boolean checkInclusion(String s1, String s2) {
int n = s1.length(), m = s2.length();
if (n > m) {
return false;
}
int[] cnt = new int[26];
for (int i = 0; i < n; ++i) {
--cnt[s1.charAt(i) - 'a'];
++cnt[s2.charAt(i) - 'a'];
}
int diff = 0;
for (int c : cnt) {
if (c != 0) {
++diff;
}
}
if (diff == 0) {
return true;
}
for (int i = n; i < m; ++i) {
int x = s2.charAt(i) - 'a', y = s2.charAt(i - n) - 'a';
if (x == y) {
continue;
}
if (cnt[x] == 0) {
++diff;
}
++cnt[x];
if (cnt[x] == 0) {
--diff;
}
if (cnt[y] == 0) {
++diff;
}
--cnt[y];
if (cnt[y] == 0) {
--diff;
}
if (diff == 0) {
return true;
}
}
return false;
}
}
复杂度分析
时间复杂度:O(n+m+∣Σ∣),其中 n 是字符串 s1 的长度,m 是字符串 s2 的长度,Σ 是字符集,这道题中的字符集是小写字母,∣Σ∣=26。
空间复杂度:O(∣Σ∣)。
方法三:双指针
算法思想
回顾方法一的思路,我们在保证区间长度为 n 的情况下,去考察是否存在一个区间使得 cnt的值全为 0。
反过来,还可以在保证 cnt 的值不为正的情况下,去考察是否存在一个区间,其长度恰好为 n。
初始时,仅统计 s1 中的字符,则 cnt 的值均不为正,且元素值之和为 −n。
然后用两个指针 left 和 right 表示考察的区间 [left,right]。right 每向右移动一次,就统计一次进入区间的字符 x。为保证 cnt 的值不为正,若此时 cnt[x]>0,则向右移动左指针,减少离开区间的字符的 cnt 值直到 cnt[x]≤0。
注意到 [left,right]的长度每增加 1,cnt 的元素值之和就增加 1。当 [left,right]的长度恰好为 n时,就意味着 cnt 的元素值之和为 0。由于 cnt 的值不为正,元素值之和为 0 就意味着所有元素均为 0,这样我们就找到了一个目标子串。
JAVA代码
class Solution {
public boolean checkInclusion(String s1, String s2) {
int n = s1.length(), m = s2.length();
if (n > m) {
return false;
}
int[] cnt = new int[26];
for (int i = 0; i < n; ++i) {
--cnt[s1.charAt(i) - 'a'];
}
int left = 0;
for (int right = 0; right < m; ++right) {
int x = s2.charAt(right) - 'a';
++cnt[x];
while (cnt[x] > 0) {
--cnt[s2.charAt(left) - 'a'];
++left;
}
if (right - left + 1 == n) {
return true;
}
}
return false;
}
}
复杂度分析
时间复杂度:O(n+m+∣Σ∣)。
创建 cnt 需要 O(∣Σ∣) 的时间。
遍历 s1 需要 O(n) 的时间。
双指针遍历 s2 时,由于 left 和 right 都只会向右移动,故这一部分需要 O(m) 的时间。
空间复杂度:O(∣Σ∣)。
链接:https://leetcode-cn.com/problems/permutation-in-string/solution/zi-fu-chuan-de-pai-lie-by-leetcode-solut-7k7u/
来源:力扣(LeetCode)