字符串中的变位词 | 循序递进---@二十一画

题目:

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。

换句话说,第一个字符串的排列之一是第二个字符串的 子串

示例 1:

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例 2:

输入: s1= "ab" s2 = "eidboaoo"
输出: False

提示:

  • 1 <= s1.length, s2.length <= 10^4
  • s1s2 仅包含小写字母

分析:

相似题目:
拆解关键词:

【s1排列组合、属于s2的字串】

想法:
1、暴力法:

列举给定s1的全部排列组合,将拿到的结果去一一寻找在s2中是否存在?

image.png

上述只列举了3个元素的情况,如果是多个元素m,那么多元素m的全部排列应该是这样:

A代表排列【P也可以代表排列】

图中公式是排列的求解,可以看出,如果使用暴力破解,遍历组合的个数如下:

image.png

随着元素的增加,这个复杂度只会越来越高,而且是按照平方次的上升趋势增加复杂度,所以暴力破解不太合适。

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} {ele1ele2diff+1ele1diffele1ele2ele2ele1使diff+1ele2ele1使diff1
②当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} {ele1ele2diff1ele1diffele1ele2ele2ele1ele2使diff1ele2ele1ele2使diff1
如上便是移动指针时需要维护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]=>diff1=>diff+1right[ele2]=>ele1[right+1]<=ele2[right+1]ele1[right+1]>ele2[right+1]=>diff+1=>diff1
最后我们只需要判断diff的值是否为0即可。

代码:

第一版:滑动窗口V1

image.png

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 + 数组下标代值【我随便起的方法,忘记这个方法叫什么名字了😂】

image.png

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 + 单变量对比数组

image.png

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只可以表示数组有几个地方不一样,无法表示是哪里不一样。

大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值