字符串中的变位词
题目:
给定两个字符串
s1
和s2
,写一个函数来判断s2
是否包含s1
的某个变位词。换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
示例 1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例 2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
提示:
1 <= s1.length, s2.length <= 10^4
s1
和s2
仅包含小写字母
分析:
相似题目:
拆解关键词:
【s1排列组合、属于s2的字串】
想法:
1、暴力法:
列举给定s1的全部排列组合,将拿到的结果去一一寻找在s2中是否存在?
上述只列举了3个元素的情况,如果是多个元素m,那么多元素m的全部排列应该是这样:
A代表排列【P也可以代表排列】
图中公式是排列的求解,可以看出,如果使用暴力破解,遍历组合的个数如下:
随着元素的增加,这个复杂度只会越来越高,而且是按照平方次的上升趋势增加复杂度,所以暴力破解不太合适。
2、滑动窗口V1
给定s1,在s2中寻找是否存在s1的任意排列之一,如果存在,那么返回真,否则返回假。
如果s1 是 “bca”,那么主要在s2中可以寻找到大小为3的连续子序列包含“abc”,那么便可以证明s1的任意子序列包含在s2中。
这样一来,就变成了一个寻找s2的一个指定大小为s1长度的连续子序列,该子序列包含了s1的全部元素,如果有这么一个解,那么就可以返回真,如果遍历结束都没解,那么返回假。
具体实现思路:
- 定义双指针,开始将指针的大小就调节为s1的大小
- 遍历每一次的窗口,判断窗口内元素是否和s1元素一致,如果一致返回true,如果不一致,那么left++,right++,窗口继续按照s1的大小右移
- 重复逻辑2,直到找到结果返回true,或者遍历到s2末尾,最后返回false
3、滑动窗口V2
滑动窗口虽好,但是v1版本中,可以分析一下,时间复杂度的问题。
窗口滑动的时间复杂度可以求得:s2.len - s1.len 次【从i=0开始遍历,到s2.len-s1.len】
另外加上对比两个子序列是否相同,还要加上HashMap存取的时间复杂度 + 两个序列逐一对比的执行次数 😓😓晕了晕了
解决方式:
可以使用分别使用两个长度为26的数组来表示每一个元素是否出现以及出现的个数,这样一来就省去了元素排序的过程。
其他地方的逻辑我们可以暂时不变,来看下效果可以提高多少?
4、滑动窗口V3
版本v2相较于v1效率方面其实提高很多,但是发现,其实每次窗口滑动的时候,每一次滑动都要调用一次isEqual方法,另外就是始终在维护两个数组ele1和ele2。
解决方式:
在滑动过程中,维护一个变量,通过变量来判断当前两个子序列是否相同。
这个变量是从ele1和ele2中的初始化计算得出,后续ele1和ele2变化过程中,不断来调节该变量,维护变量,使得通过变量可以知道当前两个子序列元素是否相同。
举例:
s1: "ab"
s2: "eidbaooo"
1、初始化:ele1【a,b】 ,ele2【e,i】,因为四个元素都不一样,所以ele1和ele2对应的26个元素下标肯定也不一样,其中a b e i这四个位置上两个数组是不一致的,除了这四个位置,其他位置因为没有元素,所以都是0。
Ele1:[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] a b存在,所以index=0和1的时候有值,ab只出现一次,所以ele1[0] ele1[1]都是1
Ele2:[0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 和上同理。
此时,有一个变量也就是上文所说的变量diff,表示两个数组之前的差,这里差就是不一样的元素个数,可以得出此时diff=4,因为有四个位置不一样。【注意,如果ele是 a a b,那么此时diff=5,因为四个元素位置不一样的基础上,个数也不一样,个数也要算diff】
Ele2:[0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
Ele1:[2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
上面这个diff=5,因为a出现了两个,算作2个差距 ,2 + 其余的3个(b e i) = 5
2、初始化之后,可以得出diff!=0,那么两个序列肯定不一样哈。然后开始遍历
ele1是恒定不变的,所以遍历只需要遍历ele2即可。【此时还是需要left 和 right指针】
首先明确,循环的时候,是right++,left–,这里始终可以保持s2的子序列长度是等同于s1的长度的。
①当right++,那么需要判断s2[right+1]位置是什么元素,下面我就直接用新元素来代表了,新元素就是 s2中的right+1位置的元素:
{
如
果
e
l
e
1
中
没
有
包
含
新
元
素
,
那
么
e
l
e
2
加
上
这
个
新
元
素
,
d
i
f
f
必
定
+
1
,
因
为
又
增
加
了
一
个
不
同
点
如
果
e
l
e
1
中
包
含
了
新
元
素
,
那
么
d
i
f
f
的
变
化
取
决
于
e
l
e
1
的
新
元
素
和
e
l
e
2
新
元
素
的
个
数
大
小
,
如
果
e
l
e
2
本
身
新
元
素
个
数
大
于
等
于
e
l
e
1
,
那
么
再
新
加
一
个
,
会
使
差
距
变
大
,
也
就
是
d
i
f
f
+
1
;
如
果
e
l
e
2
个
数
比
e
l
e
1
小
,
那
么
加
新
元
素
,
会
使
差
距
变
小
,
也
就
是
d
i
f
f
−
1
\begin{cases} 如果ele1中没有包含新元素,那么ele2加上这个新元素,diff必定+1,因为又增加了一个不同点\\ 如果ele1中包含了新元素,那么diff的变化取决于ele1的新元素和ele2新元素的个数大小,如果ele2本身新元素个数大于等于ele1,那么再新加一个,会使差距变大,也就是diff+1;如果ele2个数比ele1小,那么加新元素,会使差距变小,也就是diff-1\\ \end{cases}
{如果ele1中没有包含新元素,那么ele2加上这个新元素,diff必定+1,因为又增加了一个不同点如果ele1中包含了新元素,那么diff的变化取决于ele1的新元素和ele2新元素的个数大小,如果ele2本身新元素个数大于等于ele1,那么再新加一个,会使差距变大,也就是diff+1;如果ele2个数比ele1小,那么加新元素,会使差距变小,也就是diff−1
②当left++,那么需要判断s2[left]的位置是什么元素,因为元素要从ele2中去掉了,那么我这里称为旧元素
{
如
果
e
l
e
1
中
没
有
包
含
旧
元
素
,
那
么
e
l
e
2
去
掉
这
个
新
元
素
,
d
i
f
f
必
定
−
1
,
因
为
此
时
少
了
一
个
不
同
点
如
果
e
l
e
1
中
包
含
了
旧
元
素
,
那
么
d
i
f
f
的
变
化
取
决
于
e
l
e
1
的
旧
元
素
和
e
l
e
2
中
旧
元
素
的
个
数
大
小
,
如
果
e
l
e
2
的
个
数
小
于
等
于
e
l
e
1
,
那
么
e
l
e
2
去
掉
一
个
会
使
得
d
i
f
f
增
加
1
,
如
果
e
l
e
2
的
个
数
大
于
e
l
e
1
,
那
么
e
l
e
2
去
掉
该
元
素
,
会
使
得
d
i
f
f
−
1
,
因
为
去
掉
一
个
不
同
点
\begin{cases} 如果ele1中没有包含旧元素,那么ele2去掉这个新元素,diff必定-1,因为此时少了一个不同点\\ 如果ele1中包含了旧元素,那么diff的变化取决于ele1的旧元素和ele2中旧元素的个数大小,如果ele2的个数小于等于ele1,那么ele2去掉一个会使得diff增加1,如果ele2的个数大于ele1,那么ele2去掉该元素,会使得diff-1,因为去掉一个不同点\\ \end{cases}
{如果ele1中没有包含旧元素,那么ele2去掉这个新元素,diff必定−1,因为此时少了一个不同点如果ele1中包含了旧元素,那么diff的变化取决于ele1的旧元素和ele2中旧元素的个数大小,如果ele2的个数小于等于ele1,那么ele2去掉一个会使得diff增加1,如果ele2的个数大于ele1,那么ele2去掉该元素,会使得diff−1,因为去掉一个不同点
如上便是移动指针时需要维护diff的操作。
⚠️总结一下:
x
=
{
l
e
f
t
指
针
[
e
l
e
2
去
掉
旧
元
素
]
=
>
{
e
l
e
1
[
l
e
f
t
]
<
e
l
e
2
[
l
e
f
t
]
=
>
d
i
f
f
−
1
e
l
e
1
[
l
e
f
t
]
>
=
e
l
e
2
[
l
e
f
t
]
=
>
d
i
f
f
+
1
r
i
g
h
t
指
针
[
e
l
e
2
增
加
新
元
素
]
=
>
{
e
l
e
1
[
r
i
g
h
t
+
1
]
<
=
e
l
e
2
[
r
i
g
h
t
+
1
]
=
>
d
i
f
f
+
1
e
l
e
1
[
r
i
g
h
t
+
1
]
>
e
l
e
2
[
r
i
g
h
t
+
1
]
=
>
d
i
f
f
−
1
x = \begin{cases} left指针[ele2去掉旧元素] => \begin{cases} ele1[left]<ele2[left] &\text=> diff-1 \\\\ ele1[left]>=ele2[left] &\text=> diff+1 \\ \end{cases}\\\\ right指针[ele2增加新元素] => \begin{cases} ele1[right+1]<=ele2[right+1] &\text=> diff+1 \\\\ ele1[right+1]>ele2[right+1] &\text=> diff-1 \\ \end{cases}\\ \end{cases}
x=⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧left指针[ele2去掉旧元素]=>⎩⎪⎨⎪⎧ele1[left]<ele2[left]ele1[left]>=ele2[left]=>diff−1=>diff+1right指针[ele2增加新元素]=>⎩⎪⎨⎪⎧ele1[right+1]<=ele2[right+1]ele1[right+1]>ele2[right+1]=>diff+1=>diff−1
最后我们只需要判断diff的值是否为0即可。
代码:
第一版:滑动窗口V1
class Solution {
public boolean checkInclusion(String s1, String s2) {
//初始化
int len2 = s2.length();
int len1 = s1.length();
int left = 0;
//如果s2长度小于s1,返回-1.否则赋值right为len1-1,长度-1是下标的位置
int right = len2>=len1?len1-1:-1;
//异常情况直接返回结果 false
if(right==-1)return false;
//开始循环
while(left<=right && right<len2){
boolean isE = isEqual(s1,s2.substring(left,right+1));//左闭右开,需要right+1才能囊括right下标的值
if(isE)return true;
//否则 继续滑动指针
right++;
left++;
}
return false;
}
public static boolean isEqual(String s1,String s2){
char[] ch1 = s1.toCharArray();
char[] ch2 = s2.toCharArray();
HashMap<Character,Integer> map = new HashMap<>();
//ch2的元素依次放入map
for(char tmp:ch2){
int cnt = map.getOrDefault(tmp,0);
map.put(tmp,cnt+1);
}
//map中存放了ch2的元素,ch1的元素依次去判断是否存在于ch2中,因为ch1和ch2的长度一直,如果元素也相同的话,那么ch1遍历结束后,应该map中的元素的个数都=0.【ch1要取的 ch2都有,且个数对应】
//如果遍历过程中,ch1要取元素,但是ch2没有的话,证明两个数组不相同,直接返回false
for(char tmp:ch1){
int cnt = map.getOrDefault(tmp,0);
if(cnt ==0)return false;
map.put(tmp,cnt-1);
}
return true;
}
}
第二版:滑动窗口V2 + 数组下标代值【我随便起的方法,忘记这个方法叫什么名字了😂】
class Solution {
public boolean checkInclusion(String s1, String s2) {
//初始化
int len2 = s2.length();
int len1 = s1.length();
int left = 0;
// 下标为0 代表 'a' 下标为25 代表 'z'
int[] ele1 = new int[26];
int[] ele2 = new int[26];
//如果s2长度小于s1,返回-1.否则赋值right为len1-1,长度-1是下标的位置
int right = len2>=len1?len1-1:-1;
//异常情况直接返回结果 false
if(right==-1)return false;
//将前几个元素 推入数组 ele1 ele2
for(int i=0;i<=right;i++){
ele1[s1.charAt(i) -'a']++; // 若 a出现2次,那么 ele1[0] = 2 ele1[i] 代表 i+’a‘对应元素出现的次数 来对比两个数组元素是否一致
ele2[s2.charAt(i) -'a']++;
}
//开始循环
while(left<=right && right<len2){
boolean isE = isEqual(ele1,ele2);
if(isE)return true;
//否则 继续滑动指针 且 将数组之前left 的元素去掉
ele2[s2.charAt(left)-'a']--;
left++;
right++;
if(right<len2) ele2[s2.charAt(right)-'a']++;
}
return false;
}
public static boolean isEqual(int[] ele1,int[] ele2){
for(int i=0;i<26;i++){
if(ele1[i]!=ele2[i]) return false;
}
return true;
}
}
第三版:滑动窗口V3 + 单变量对比数组
class Solution {
public boolean checkInclusion(String s1, String s2) {
//初始化
int len2 = s2.length();
int len1 = s1.length();
int left = 0;
int diff = 0;
// 下标为0 代表 'a' 下标为25 代表 'z'
int[] ele1 = new int[26];
int[] ele2 = new int[26];
//如果s2长度小于s1,返回-1.否则赋值right为len1-1,长度-1是下标的位置
int right = len2>=len1?len1-1:-1;
//异常情况直接返回结果 false
if(right==-1)return false;
//将前几个元素 推入数组 ele1 ele2
for(int i=0;i<=right;i++){
ele1[s1.charAt(i) -'a']++; // 若 a出现2次,那么 ele1[0] = 2 ele1[i] 代表 i+’a‘对应元素出现的次数 来对比两个数组元素是否一致
ele2[s2.charAt(i) -'a']++;
}
//初始化 diff值
for(int i=0;i<26;i++){
if(ele1[i]!=ele2[i]) diff += Math.abs(ele1[i]-ele2[i]);
}
//开始循环
while(left<=right && right<len2){
if(diff==0)return true; //如果diff=0 那么返回true
//left指针滑动
if(ele1[s2.charAt(left) -'a']<ele2[s2.charAt(left) -'a']) diff-=1; else diff+=1;
//修改ele2
ele2[s2.charAt(left) -'a']-=1;
left++;
//right指针移动
right++;
if(right<len2){
if(ele1[s2.charAt(right) -'a']<=ele2[s2.charAt(right) -'a']) diff+=1; else diff-=1;
//修改ele2
ele2[s2.charAt(right) -'a']+=1;
}
}
return false;
}
}
总结:
①暴力破解去遍历所给元素的每一种可能组成形式和s2数组去判重,确实工作量太大了,且在本题给定时间空间内根本无法完成。
②滑动窗口法运用的前提是,必须要想到s1和s2为解的内在关系,就是指s1和s2他们的元素一致,且元素出现的个数一致,这样就转化为了一个在指定窗口大小去寻找是否满足s1的问题。故可以使用滑动窗口求解。
③当判断一个乱序数组是否和另外乱序数组内元素一致,或者说一个乱序数组内的元素个数是否和另外一个乱序数组内元素个数一致,说真的,数组下标代值真的是一个很不错的选择,避免了需要去对数据排序的过程,而是将有限出现的元素转化为了下标的表示,下标对应的值来表示该元素是否存在,甚至可以表示该元素出现了几次。
④在滑动数组+对比数组差距的时候,完全可以引入一个单变量来维护数组差异值的变化diff,因为滑动数组是规律的,且对比数组变化也是连续的,这种情况下只要diff初始逻辑没问题,那么后续都可以按照这个来实现。diff只可以表示数组有几个地方不一样,无法表示是哪里不一样。
大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊