原文链接: https://leetcode-cn.com/problems/permutation-in-string
英文原文
Given two strings s1
and s2
, return true
if s2
contains a permutation of s1
, or false
otherwise.
In other words, return true
if one of s1
's permutations is the substring of s2
.
Example 1:
Input: s1 = "ab", s2 = "eidbaooo" Output: true Explanation: s2 contains one permutation of s1 ("ba").
Example 2:
Input: s1 = "ab", s2 = "eidboaoo" Output: false
Constraints:
1 <= s1.length, s2.length <= 104
s1
ands2
consist of lowercase English letters.
中文题目
给你两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。如果是,返回 true
;否则,返回 false
。
换句话说,s1
的排列之一是 s2
的 子串 。
示例 1:
输入:s1 = "ab" s2 = "eidbaooo" 输出:true 解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo" 输出:false
提示:
1 <= s1.length, s2.length <= 104
s1
和s2
仅包含小写字母
通过代码
高赞题解
方法一:滑动窗口
由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。
根据这一性质,记 $s_1$ 的长度为 $n$,我们可以遍历 $s_2$ 中的每个长度为 $n$ 的子串,判断子串和 $s_1$ 中每个字符的个数是否相等,若相等则说明该子串是 $s_1$ 的一个排列。
使用两个数组 $\textit{cnt}_1$ 和 $\textit{cnt}_2$,$\textit{cnt}_1$ 统计 $s_1$ 中各个字符的个数,$\textit{cnt}_2$ 统计当前遍历的子串中各个字符的个数。
由于需要遍历的子串长度均为 $n$,我们可以使用一个固定长度为 $n$ 的滑动窗口来维护 $\textit{cnt}_2$:滑动窗口每向右滑动一次,就多统计一次进入窗口的字符,少统计一次离开窗口的字符。然后,判断 $\textit{cnt}_1$ 是否与 $\textit{cnt}_2$ 相等,若相等则意味着 $s_1$ 的排列之一是 $s_2$ 的子串。
[sol11-C++]class Solution { public: bool checkInclusion(string s1, string s2) { int n = s1.length(), m = s2.length(); if (n > m) { return false; } vector<int> cnt1(26), cnt2(26); for (int i = 0; i < n; ++i) { ++cnt1[s1[i] - 'a']; ++cnt2[s2[i] - 'a']; } if (cnt1 == cnt2) { return true; } for (int i = n; i < m; ++i) { ++cnt2[s2[i] - 'a']; --cnt2[s2[i - n] - 'a']; if (cnt1 == cnt2) { return true; } } return false; } };
[sol11-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; } }
[sol11-Golang]func checkInclusion(s1, s2 string) bool { n, m := len(s1), len(s2) if n > m { return false } var cnt1, cnt2 [26]int for i, ch := range s1 { cnt1[ch-'a']++ cnt2[s2[i]-'a']++ } if cnt1 == cnt2 { return true } for i := n; i < m; i++ { cnt2[s2[i]-'a']++ cnt2[s2[i-n]-'a']-- if cnt1 == cnt2 { return true } } return false }
[sol11-C]bool equals(int* cnt1, int* cnt2) { for (int i = 0; i < 26; i++) { if (cnt1[i] != cnt2[i]) { return false; } } return true; } bool checkInclusion(char* s1, char* s2) { int n = strlen(s1), m = strlen(s2); if (n > m) { return false; } int cnt1[26], cnt2[26]; memset(cnt1, 0, sizeof(cnt1)); memset(cnt2, 0, sizeof(cnt2)); for (int i = 0; i < n; ++i) { ++cnt1[s1[i] - 'a']; ++cnt2[s2[i] - 'a']; } if (equals(cnt1, cnt2)) { return true; } for (int i = n; i < m; ++i) { ++cnt2[s2[i] - 'a']; --cnt2[s2[i - n] - 'a']; if (equals(cnt1, cnt2)) { return true; } } return false; }
[sol11-JavaScript]var checkInclusion = function(s1, s2) { const n = s1.length, m = s2.length; if (n > m) { return false; } const cnt1 = new Array(26).fill(0); const cnt2 = new Array(26).fill(0); for (let i = 0; i < n; ++i) { ++cnt1[s1[i].charCodeAt() - 'a'.charCodeAt()]; ++cnt2[s2[i].charCodeAt() - 'a'.charCodeAt()]; } if (cnt1.toString() === cnt2.toString()) { return true; } for (let i = n; i < m; ++i) { ++cnt2[s2[i].charCodeAt() - 'a'.charCodeAt()]; --cnt2[s2[i - n].charCodeAt() - 'a'.charCodeAt()]; if (cnt1.toString() === cnt2.toString()) { return true; } } return false; };
优化
注意到每次窗口滑动时,只统计了一进一出两个字符,却比较了整个 $\textit{cnt}_1$ 和 $\textit{cnt}_2$ 数组。
从这个角度出发,我们可以用一个变量 $\textit{diff}$ 来记录 $\textit{cnt}_1$ 与 $\textit{cnt}_2$ 的不同值的个数,这样判断 $\textit{cnt}_1$ 和 $\textit{cnt}_2$ 是否相等就转换成了判断 $\textit{diff}$ 是否为 $0$.
每次窗口滑动,记一进一出两个字符为 $x$ 和 $y$.
若 $x=y$ 则对 $\textit{cnt}_2$ 无影响,可以直接跳过。
若 $x\ne y$,对于字符 $x$,在修改 $\textit{cnt}_2$ 之前若有 $\textit{cnt}_2[x]=\textit{cnt}_1[x]$,则将 $\textit{diff}$ 加一;在修改 $\textit{cnt}_2$ 之后若有 $\textit{cnt}_2[x]=\textit{cnt}_1[x]$,则将 $\textit{diff}$ 减一。字符 $y$ 同理。
此外,为简化上述逻辑,我们可以只用一个数组 $\textit{cnt}$,其中 $\textit{cnt}[x]=\textit{cnt}_2[x]-\textit{cnt}_1[x]$,将 $\textit{cnt}_1[x]$ 与 $\textit{cnt}_2[x]$ 的比较替换成 $\textit{cnt}[x]$ 与 $0$ 的比较。
[sol12-C++]class Solution { public: bool checkInclusion(string s1, string s2) { int n = s1.length(), m = s2.length(); if (n > m) { return false; } vector<int> cnt(26); for (int i = 0; i < n; ++i) { --cnt[s1[i] - 'a']; ++cnt[s2[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[i] - 'a', y = s2[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; } };
[sol12-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; } }
[sol12-Golang]func checkInclusion(s1, s2 string) bool { n, m := len(s1), len(s2) if n > m { return false } cnt := [26]int{} for i, ch := range s1 { cnt[ch-'a']-- cnt[s2[i]-'a']++ } diff := 0 for _, c := range cnt[:] { if c != 0 { diff++ } } if diff == 0 { return true } for i := n; i < m; i++ { x, y := s2[i]-'a', s2[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 }
[sol12-C]bool checkInclusion(char* s1, char* s2) { int n = strlen(s1), m = strlen(s2); if (n > m) { return false; } int cnt[26]; memset(cnt, 0, sizeof(cnt)); for (int i = 0; i < n; ++i) { --cnt[s1[i] - 'a']; ++cnt[s2[i] - 'a']; } int diff = 0; for (int i = 0; i < 26; ++i) { if (cnt[i] != 0) { ++diff; } } if (diff == 0) { return true; } for (int i = n; i < m; ++i) { int x = s2[i] - 'a', y = s2[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; }
[sol12-JavaScript]var checkInclusion = function(s1, s2) { const n = s1.length, m = s2.length; if (n > m) { return false; } const cnt = new Array(26).fill(0); for (let i = 0; i < n; ++i) { --cnt[s1[i].charCodeAt() - 'a'.charCodeAt()]; ++cnt[s2[i].charCodeAt() - 'a'.charCodeAt()]; } let diff = 0; for (const c of cnt) { if (c !== 0) { ++diff; } } if (diff == 0) { return true; } for (let i = n; i < m; ++i) { const x = s2[i].charCodeAt() - 'a'.charCodeAt(), y = s2[i - n].charCodeAt() - 'a'.charCodeAt(); 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+|\Sigma|)$,其中 $n$ 是字符串 $s_1$ 的长度,$m$ 是字符串 $s_2$ 的长度,$\Sigma$ 是字符集,这道题中的字符集是小写字母,$|\Sigma|=26$。
空间复杂度:$O(|\Sigma|)$。
方法二:双指针
回顾方法一的思路,我们在保证区间长度为 $n$ 的情况下,去考察是否存在一个区间使得 $\textit{cnt}$ 的值全为 $0$。
反过来,还可以在保证 $\textit{cnt}$ 的值不为正的情况下,去考察是否存在一个区间,其长度恰好为 $n$。
初始时,仅统计 $s_1$ 中的字符,则 $\textit{cnt}$ 的值均不为正,且元素值之和为 $-n$。
然后用两个指针 $\textit{left}$ 和 $\textit{right}$ 表示考察的区间 $[\textit{left},\textit{right}]$。$\textit{right}$ 每向右移动一次,就统计一次进入区间的字符 $x$。为保证 $\textit{cnt}$ 的值不为正,若此时 $\textit{cnt}[x]>0$,则向右移动左指针,减少离开区间的字符的 $\textit{cnt}$ 值直到 $\textit{cnt}[x] \le 0$。
注意到 $[\textit{left},\textit{right}]$ 的长度每增加 $1$,$\textit{cnt}$ 的元素值之和就增加 $1$。当 $[\textit{left},\textit{right}]$ 的长度恰好为 $n$ 时,就意味着 $\textit{cnt}$ 的元素值之和为 $0$。由于 $\textit{cnt}$ 的值不为正,元素值之和为 $0$ 就意味着所有元素均为 $0$,这样我们就找到了一个目标子串。
[sol2-C++]class Solution { public: bool checkInclusion(string s1, string s2) { int n = s1.length(), m = s2.length(); if (n > m) { return false; } vector<int> cnt(26); for (int i = 0; i < n; ++i) { --cnt[s1[i] - 'a']; } int left = 0; for (int right = 0; right < m; ++right) { int x = s2[right] - 'a'; ++cnt[x]; while (cnt[x] > 0) { --cnt[s2[left] - 'a']; ++left; } if (right - left + 1 == n) { return true; } } return false; } };
[sol2-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; } }
[sol2-Golang]func checkInclusion(s1, s2 string) bool { n, m := len(s1), len(s2) if n > m { return false } cnt := [26]int{} for _, ch := range s1 { cnt[ch-'a']-- } left := 0 for right, ch := range s2 { x := ch - 'a' cnt[x]++ for cnt[x] > 0 { cnt[s2[left]-'a']-- left++ } if right-left+1 == n { return true } } return false }
[sol2-C]bool checkInclusion(char* s1, char* s2) { int n = strlen(s1), m = strlen(s2); if (n > m) { return false; } int cnt[26]; memset(cnt, 0, sizeof(cnt)); for (int i = 0; i < n; ++i) { --cnt[s1[i] - 'a']; } int left = 0; for (int right = 0; right < m; ++right) { int x = s2[right] - 'a'; ++cnt[x]; while (cnt[x] > 0) { --cnt[s2[left] - 'a']; ++left; } if (right - left + 1 == n) { return true; } } return false; }
[sol2-JavaScript]var checkInclusion = function(s1, s2) { const n = s1.length, m = s2.length; if (n > m) { return false; } const cnt = new Array(26).fill(0); for (let i = 0; i < n; ++i) { --cnt[s1[i].charCodeAt() - 'a'.charCodeAt()]; } let left = 0; for (let right = 0; right < m; ++right) { const x = s2[right].charCodeAt() - 'a'.charCodeAt(); ++cnt[x]; while (cnt[x] > 0) { --cnt[s2[left].charCodeAt() - 'a'.charCodeAt()]; ++left; } if (right - left + 1 === n) { return true; } } return false; };
复杂度分析
时间复杂度:$O(n+m+|\Sigma|)$。
创建 $\textit{cnt}$ 需要 $O(|\Sigma|)$ 的时间。
遍历 $s_1$ 需要 $O(n)$ 的时间。
双指针遍历 $s_2$ 时,由于 $\textit{left}$ 和 $\textit{right}$ 都只会向右移动,故这一部分需要 $O(m)$ 的时间。空间复杂度:$O(|\Sigma|)$。
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
133495 | 308067 | 43.3% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|
相似题目
题目 | 难度 |
---|---|
最小覆盖子串 | 困难 |
找到字符串中所有字母异位词 | 中等 |