算法 ---- 滑动窗口(双指针)

题目描述:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-auxWNlTU-1613958086635)(面试题photo/替换后的最长重复字符.png)]


解题思路:
  • 暴力解法:

    如果一个问题暂时没有思路,可以先考虑暴力解法(不一定要实现)。

    当前问题的暴力解法是:枚举输入字符串的 所有 子串,对于每一个子串

    • 如果子串里所有的字符都一样,就考虑长度更长的子串;
    • 如果当前子串里出现了至少两种字符,要想使得替换以后所有的字符都一样,并且重复的、连续的部分更长,应该替换掉出现次数最多字符 以外 的字符。

    暴力解法的时间复杂度为 O(N3)(这里 N 是输入字符串的长度,枚举所有子串 O(N2),对于每一个子串计算最多出现的字符 O(N))。而题目的提示告诉我们 字符串长度 和 k 不会超过 104,暴力算法在这个数据规模下会超时。

    暴力解法的缺点:

    • 做了重复的工作,子串和子串有很多重合的部分,重复扫描它们是不划算的;
    • 做了很多没有必要的工作:
      1. 如果找到了一个长度为 L 且替换 k 个字符以后全部相等的子串,就没有必要考虑长度小于等于 L 的子串,因为题目只让我们找到符合题意的最长的长度;
      2. 如果找到了一个长度为 L 且替换 k 个字符以后不能全部相等的子串,左边界相同、长度更长的子串一定不符合要求(原因我们放在最后说)。
  • 滑动窗口(双指针):

    「滑动窗口」是一类使用「双指针」技巧,通过两个变量在数组上同向交替移动解决问题的算法。

    s = AABCABBBk = 2 为例,寻找替换 k 次以后字符全部相等的最长子串的长度的过程如下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zbN0wfiU-1613958086637)(面试题photo/字符串.gif)]

    整个过程,我们使用了两个表示边界的变量,一前一后,交替在字符串上前进:右边界先向右和移动,直到它不能移动了为止,左边界再继续向右移动,整个过程像极了一个滑动的窗口在一条线段上移动。

    我们还一直关心的是:考虑的子串中最多出现的字符是次数,因此须要一个频数数组,记录每个字符出现的次数。

    解题步骤:

    1. 右边界先移动找到一个满足题意的可以替换 k 个字符以后,所有字符都变成一样的当前看来最长的子串,直到右边界纳入一个字符以后,不能满足的时候停下;
    2. 然后考虑左边界向右移动,左边界只须要向右移动一格以后,右边界就又可以开始向右移动了,继续尝试找到更长的目标子串;
    3. 替换后的最长重复子串就产生在右边界、左边界交替向右移动的过程中。

    滑动窗口这里实在不会,那就只能背下来了

    public class Solution {
        public int characterReplacement(String s, int k) {  // k=2
            int len = s.length();
            if (len < 2) {
                return len;
            }
    
            char[] charArray = s.toCharArray();  // AABCABBB
            int left = 0;
            int right = 0;
    
            int res = 0;
            int maxCount = 0;
            int[] freq = new int[26];
            // [left, right) 内最多替换 k 个字符可以得到只有一种字符的子串
            while (right < len){
                freq[charArray[right] - 'A']++;
                // 在这里维护 maxCount,因为每一次右边界读入一个字符,字符频数增加,才会使得 maxCount 增加
                maxCount = Math.max(maxCount, freq[charArray[right] - 'A']);
                right++;
    
                if (right - left > maxCount + k){
                  	// 说明此时 k 不够用
                    // 把其它不是最多出现的字符替换以后,都不能填满这个滑动的窗口,这个时候须要考虑左边界向右移动
                    // 移出滑动窗口的时候,频数数组须要相应地做减法
                    freq[charArray[left] - 'A']--;
                    left++;
                }
                res = Math.max(res, right - left);
            }
            return res;
        }
    }
    

    复杂度分析:

    • 时间复杂度:O(N),这里 N 是输入字符串 S 的长度;
    • 空间复杂度:O(∣Σ∣),这里 ∣Σ∣ 是输入字符串 S 出现的字符种类数。

以下是我们在编码的过程中思考的一些问题:

1. 证明:如果长度为 L 的子串不符合题目的要求,那么左边界固定,长度更长的子串也不符合题目的要求。

答:记 count(X) 表示长度为 L 的子串中,字符 X 出现的次数。

不失一般性,假设长度为 L 的子串,出现最多的字符为 A,记 count(A) = x。其余字符均为 B,记 count(B) = y。由字符 A 出现次数最多,可知 x≥y。又由于长度为 L 的子串不符合题目的要求,可知 y>k。起点固定的情况下,考虑更长的子串:

  • 如果接下来看到的字符都是 A(频数最多的字符越来越多),依然须要考虑把之前看到的 B 全部替换成为 A,由于 count(B) = y > k,这是不能做到的;
  • 如果接下来看到的字符不是 A(频数较少的字符超过原来频数最多的字符),那么须要考虑把之前看到的 A 全部替换成为新的频数最多的字符,由于 count(A)=x≥y>k,这也是不能做到的。

2. maxCount 在内层循环「左边界向右移动一个位置」的过程中,没有维护它的定义,结论是否正确?

答:结论依然正确。「左边界向右移动一个位置」的时候,maxCount 或者不变,或者值减 11。

maxCount 的值虽然不维护,但数组 freq 的值是被正确维护的;
当「左边界向右移动」之前:

  • 如果有两种字符长度相等,左边界向右移动不改变 maxCount 的值。例如 s = [AAABBB]、k = 2,左边界 A 移除以后,窗口内字符出现次数不变,依然为 33;

  • 如果左边界移除以后,使得此时 maxCount 的值变小,又由于 我们要找的只是最长替换 k 次以后重复子串的长度。接下来我们继续让右边界向右移动一格,有两种情况:

    ① 右边界如果读到了刚才移出左边界的字符,恰好 maxCount 的值被正确维护;

    ② 右边界如果读到了不是刚才移出左边界的字符,新的子串要想在符合题意的条件下变得更长,maxCount 一定要比之前的值还要更多,因此不会错过更优的解。

3. 内层循环里的 if 能不能改成 while?

答:可以但没有必要。理由依然是:我们只关心最长替换 k 次以后重复子串的长度。

正是因为多读了一个字符,使得 right - left > maxCount + k 成立;
在 left++ 以后,由于可以不维护 maxCount 的定义,right - left > maxCount + k 不成立。因此 if 里面的代码块只会被执行一次。

4. 可以不用一直用 res 记录滑动窗口的最大长度,最后返回 right - left 即可。

答:依然是 我们只关心最长替换 k 次以后重复子串的长度,并且 maxCount 只会增加不会减少。在退出内层 if 语句的时候,区间 [left, right) 不一定是符合要求的子串,但是子串的长度一定满足 right - left == maxCount + k 成立。这一点如果很难理解的话,我们建议大家使用小测试数据、跟踪代码进行理解。


这篇文章是参考LeetCode里的某位博主的解析,但由于当时临时将笔记记录在本机,时间太久,忘记是哪位博主的了,就没有附上转载链接了。若是该博主看到可联系我,我会加上附上转载链接。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值