LeetCode算法题3:滑动窗口*


前言

      Leetcode算法系列:https://leetcode-cn.com/study-plan/algorithms/?progress=njjhkd2

      简单总结一下滑动窗口相关的算法题:

一、无重复字符的最长子串

      题目链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

      题目描述:给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

思路 1:

      用一个区间(窗口)来表示当前的无重复字符串,每当区间扩大1位的时候,需要判断当前扩充的元素和区间中哪位元素发生重复,不发生重复,区间长度加一;否则从重复元素的下一个元素作为区间的起始点,当前扩充的元素作为区间的结束点。依次遍历所有元素。

    public int lengthOfLongestSubstring(String s) {
    
        int i=0,end=s.length(),start=0;//i 遍历整个字符串;start 为区间起始点。
        int tmp=0,ans=0;//tmp 为当前合法区间的长度,ans 为以往合法区间长度的最大值。

        while(i<end){
            int re=isNotDup(s,start,i);
            if(re==-1)
                tmp++;

            else{
                ans=tmp>ans?tmp:ans;
                tmp=i-re+1;
                start=re;//设置起点。
            }
            i++;
        }
        return ans>tmp?ans:tmp;
    }
    public int isNotDup(String a,int i,int j){
        while(i<j){
            if(a.charAt(i)==a.charAt(j))
                return i+1;
            i++;
        }
        return -1;
    }

      时间复杂度为 O(n2)。

思路 2:

      关于如何判断是否发生重复,以及设置区间的下一个起点,可以采用 Set 来做,如果发生重复的话:从上一个区间的起点依次将元素移出集合,直到不重复为止。参考算法如下:

    public int lengthOfLongestSubstring(String s) {

        Set<Character> container=new HashSet<>();
        int i=0,end=s.length(),start=0;
        int tmp=0,ans=0;
        while(i<end){
            if(!container.add(s.charAt(i))){
                tmp=container.size();
                ans=ans<tmp?tmp:ans;
                for(int j=start;j<i;j++){
                    container.remove(s.charAt(j));
                    if(container.add(s.charAt(i))){
                        start=j+1;
                        break;
                    }
                }
            }
            i++;
        }
        return ans>container.size()?ans:container.size();
    }

      时间复杂度为 O(n2)。但是它的执行效率没有算法1 快。

二、字符串的排列

      题目链接:https://leetcode-cn.com/problems/permutation-in-string/

      题目描述:给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。

换句话说,s1 的排列之一是 s2 的 子串 。

思路 1:

      直观上,对 s1 求得全排列,然后分别对 s1 的所有排列验证是 s2 的子串。为了回顾 Kmp 和 全排列算法,参考代码如下:(但它的时间复杂度太高,在此不建议使用)

    public boolean checkInclusion(String s1, String s2) {
        //方法1:得到s1的全排列组合, 再采用kmp判断每个s1是否为s2的子串。 超出内存限制

        if(s1.length()>s2.length())
            return false;
        ArrayList<String> re=new ArrayList<>();
        arrange(re,s1.toCharArray(),0,s1.length()-1);
        for(String tmp:re){
            if(kmp(tmp,s2))
                return true;
        }
        return false;
    }
    public static boolean kmp(String s1,String s2){ //s1是否为s2的子串
        //求next数组。
        // -1表示用s1中的第一个字符匹配s2的下一个字符;非负数表示用s1对应next中的值再次匹配当前值。
        int len=s1.length();
        int[] next=new int[len];
        next[0]=-1;
        int i=0,tmp;
        while(++i<len){
            tmp=next[i-1];
            if(tmp==-1){//简化:不需要考虑 s1(0) 和 s1(1) 相等的情况。
                next[i]=0;
                continue;
            }
            while(tmp!=-1&&s1.charAt(i-1)!=s1.charAt(tmp))
                tmp=next[tmp];

            next[i]=tmp+1;
        }

        //开始匹配
        int index1=0,index2=0;
        while(index2<s2.length()){
            while(s2.charAt(index2)!=s1.charAt(index1)){
                index1=next[index1];
                if(index1==-1)
                    break;
            }
            index2++;
            index1++;
            if(index1==len)
                return true;
        }
        return false;
    }
    //全排列:
    public  void arrange(ArrayList<String> re,char[] a,int start,int end){
        if(start==end)
            re.add(new String(a)); //采用a构建一个String对象。
        else {
            int index=start;
            char tmp;
            while(index<=end){
                tmp=a[index];
                a[index]=a[start];
                a[start]=tmp;

                arrange(re,a,start+1,end);

                tmp=a[index];
                a[index]=a[start];
                a[start]=tmp;
                index++;
            }
        }
    }

思路 2:

      以 s1 的第一个字符为基准,找到该字符在 s2 中每一次出现的位置 a0、a1、a2… ,且假设 s1 的长度为 len,那么在 a0 处从区间【a0-len+1,a0】直到【a0,a0+len-1】都有可能是 s1 的一个排列,其余的 a1,a2也是一样。对区间【a0-len+1,a0】直到【a0,a0+len-1】中的元素分别进行排序和排序后的 s1 比较判断是否相等。参考算法如下(其时间复杂度依然太高):

    public boolean checkInclusion(String s1, String s2) {
        int len1=s1.length(),len2=s2.length();
        if(len1>len2)
            return false;
        int start,end;
        for(int j=0;j<len2;j++){
            if(s1.charAt(0)==s2.charAt(j)){
                //从最左区间开始判断
                start=j-len1+1;end=j;
                if(start<0){
                    end=len1-1;
                    start=0;
                }
                for(;end<len2;start++,end++){
                    if(decide(s1,s2.substring(start,end+1)))
                        return true;
                }
            }
        }
        return false;
    
    }

    public boolean decide(String a,String b){
        char[] aa=a.toCharArray();
        char[] bb=b.toCharArray();
        Arrays.sort(aa);
        Arrays.sort(bb);
        if(Arrays.equals(aa,bb))
            return true;
        return false;
    }

思路 3:

      在 2 的基础上,对区间【a0-len+1,a0】直到【a0,a0+len-1】中的元素和 s1 的比较采用统计字符个数的方法来判断是否相等。参考算法如下(其时间复杂度也比较高):

    public boolean checkInclusion(String s1, String s2) {
        int len1=s1.length(),len2=s2.length();
        if(len1>len2)
            return false;
        int start,end;
        int[] count1=new int[26];
        int[] count2=new int[26];
        for(int j=0;j<len2;j++){
            if(s1.charAt(0)==s2.charAt(j)){
                //从最左区间开始判断
                start=j-len1+1;end=j;
                if(start<0){
                    end=len1-1;
                    start=0;
                }
                for(;end<len2;start++,end++){
                    Arrays.fill(count1,0);
                    Arrays.fill(count2,0);
                    if(decide(s1,s2.substring(start,end+1),count1,count2))
                        return true;
                }
            }
        }
        return false;
    }
    public boolean decide(String a,String b,int[] count1,int[] count2){//采用判断字符的个数是否相同的方法验证a是否为b的某个排列。
        for(int i=0;i<a.length();i++){
            count1[a.charAt(i)-'a']++;
            count2[b.charAt(i)-'a']++;
        }
        if(Arrays.equals(count1,count2))
            return true;
        return false;
    }

思路 3 plus:

      首先没必要每次都初始化数组 count1,它只需要初始化一次即可,其次对于 count2 没必要进行整体上的改变,每次只需要增加新元素的个数,减少旧元素的个数。参考算法如下:

    public boolean checkInclusion(String s1, String s2) {
        int len1=s1.length(),len2=s2.length();
        if(len1>len2)
            return false;
        int start,end;
        int[] count1=new int[26];
        for(int i=0;i<len1;i++)
            count1[s1.charAt(i)-'a']++;
        int[] count2=new int[26];

        for(int j=0;j<len2;j++){
            if(s1.charAt(0)==s2.charAt(j)){
                //从最左区间开始判断
                start=j-len1+1;end=j;
                if(start<0){
                    end=len1-1;
                    start=0;
                }
                //count2 初始化:
                Arrays.fill(count2,0);
                for(int i=start;i<=end;i++)
                    count2[s2.charAt(i)-'a']++;
                
                if(Arrays.equals(count1,count2))
                    return true;    
                int tmp=j+len1-1;
                for(start++,end++;end<len2&&end<=tmp;start++,end++){
                    count2[s2.charAt(end)-'a']++;
                    count2[s2.charAt(start-1)-'a']--;
                    if(Arrays.equals(count1,count2))
                        return true;   
                }
            }
        }
        return false;
    }

思路 3 plus plus:

      上面算法虽然相比于之前的算法有了更好的运行效率。但是此算法的效率没有直接在字符串 s2 上采用滑动窗口的方法高,原因在于上面的算法在区间有重复计算和比较。在 s2 直接采用滑动窗口的算法参考如下:

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;
    }

思路 4:

      上面算法有一个缺点:因为 ct2 每次各增减一个字符的个数,但是却对 ct1 和 ct2 整体作比较,优化一下,那么需要用一个变量 diff 来表示 ct1 和 ct2 之间的差异性,如果 diff 为 0,则二者相等,返回 true;如果不相等,需要继续在 s2 上进行增减一个字符操作。差异性可以用 ct2 - ct1 得到,如果相减之后的数组元素均为 0,则二者相等,diff 也为0,否则初始时的 diff 为相减之后数组元素不为 0 的个数。

      这个数组用 cnt 来表示。当结果返回 true 时对应的是 cnt 中的元素均为 0(diff 为 0),如果 cnt 中的数为负,意味 ct2 中的字符没有对应的 ct1 中的字符,则需要增加一个对应的字符;如果为正,意味着 ct2 中的字符是多余的,需要减去一个对应的字符。

      在每次窗口滑动时,增加一个字符需要判断 cnt[该字符]+1 之后是否为 0,如果为 0,令 diff 减一;减少的字符同理。如果 diff 为 0,证明已找到结果了。

      最终:参考算法:

    public boolean checkInclusion(String s1, String s2) {
        int len1=s1.length(),len2=s2.length();
        if(len1>len2)
            return false;
        int[] cnt=new int[26];
        int diff=0;
        
        for(int i=0;i<len1;i++){
            cnt[s2.charAt(i)-'a']++;
            cnt[s1.charAt(i)-'a']--;
        }
        for(int i=0;i<cnt.length;i++)
            if(cnt[i]!=0)
                diff++;
        
        if(diff==0)
            return true;
        for(int i=len1;i<len2;i++){
            int addIndex=s2.charAt(i)-'a',minusIndex=s2.charAt(i-len1)-'a';
            if(addIndex==minusIndex)
                continue;

            if(cnt[addIndex]==0)//如果原来的值为 0,diff需要加一。
                diff++;
            if(++cnt[addIndex]==0)//增加一个字符,对应个数加一。加完之后若为0,diff需要减一
                diff--;

            if(cnt[minusIndex]==0)
                diff++;
            if(--cnt[minusIndex]==0)
                diff--;

            if(diff==0)
                return true;
        }
        return false;
    }

思路 5:

      在思路 4 的基础上修改一下,用双指针的思路来做,初始时 left 和 right 指针均为 0,表示仅包含在 s2 上的第一个元素。初始化时 cnt 数组中的所有值要不为 0,要不小于 0(仅用 cnt[s1.charAt(i)-‘a’]–;来初始化)。

      遍历:右指针一次遍历,增加的元素如果令对应 cnt 中的数为 <= 0,继续;如果 >0 时,则在当前左右指针所构成的区间中,当前元素是多余的,此时需要让左指针不断右移,去掉一个当前元素。

      返回 true 的条件是:某一时刻 左右指针所构成的区间长度等于 s1。参考算法:

    public boolean checkInclusion(String s1, String s2) {
        int len1=s1.length(),len2=s2.length();
        if(len1>len2)
            return false;
        int[] cnt=new int[26];
        
        int left=0,right=0,addIndex;
        for(int i=0;i<len1;i++)
            cnt[s1.charAt(i)-'a']--;
        
        while(right<len2){
            addIndex=s2.charAt(right)-'a';
            ++cnt[addIndex];
            while(cnt[addIndex]>0){
                cnt[s2.charAt(left)-'a']--;
                left++;
            }
            if(right-left+1==len1)
                return true;
            right++;
        }
        return false;
    }

       效率上,最后的双指针要优于之前的解法,算法也比较简洁。


总结

      完。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值