【java算法专场】滑动窗口(上)

目录

滑动窗口

基本概念

长度最小的子数组

 算法分析

算法步骤

示例

算法代码

无重复字符的最长子串

算法分析

算法步骤

示例

算法代码

最大连续1的个数 III

算法分析

算法步骤

示例

算法代码

  将 x 减到 0 的最小操作数

算法分析

算法步骤

示例

算法代码


滑动窗口

基本概念

滑动窗口(同向双指针)是一种基于双指针的一种思想,两个指针之间构成了一个窗口。

类型:窗口一般分为两类:一、固定大小的窗口;二、大小动态变化的窗口

适用场景

  • 给的数据结构是数组/字符串;
  • 求子数组/字符子串的最长最短等最值问题或者是在某个目标值时;
  • 问题本身是可以通过暴力求解。

下面通过一些题目,来加深对滑动窗口的理解。

长度最小的子数组

 算法分析

这道题其实就是要找一个连续的最短子数组,且这个子数组中所有元素之和要大于等于目标值target。如果使用暴力枚举每一个子数组的话,时间复杂度就会达到O(N^2),这道题的话,有更好的方法,那就是使用滑动窗口,接下来就解析一下如何使用滑动窗口来解这道题。

算法步骤

  1. 初始化双指针:设置两个指针leftright并初始化为0,表示窗口的左右边界。设置一个sum并初始化为0,用于计算窗口中所有元素之和。设置min并初始化为Integer.MAX_VALUE,用于记录符合目标的最小连续数组长度。
  2. 右边界(right)移动:让right往右移动,并nums[right]加入到sum中,这个过程相当于在扩大窗口的大小,让sum的值不断接近target。
  3. 更新min值:当sum的值大于等于target之后,利用min方法,更新min的值。
  4. 左边界(right)右移:当更新完min值之后,需要让left往右移,并且让sum减去nums[left]的值。这个过程相当于在缩小窗口。当sum的值比target小时,停止右移。
  5. 重复2-3-4操作:当走完步骤4之后,继续移动右边界,循环上述操作,直到right>nums.length。
  6. 返回min值:在返回min时,需要注意一下,可能数组中的所有元素相加都不能大于等于target,这时min依然是初始值,所以在返回时,需要判断一下min值。

时间复杂度O(n),其中n为数组的长度,每个元素最多被遍历两次,第一次是扩大右边界时,第二次是左边界右移时。

空间复杂度:O(1),在算法中,只用了常数个变量。

示例

这里我们以第一个例子为例{2,3,1,4,3},target=7

第一步:初始值

 第二步:右边界右移

第三步:更新min值

此时min值为4

第四步:左边界右移

第五步:重复2-3-4步,直到遍历完整个数组

第六步:返回min值 【4,3】

算法代码

class Solution {
     public int minSubArrayLen(int target, int[] nums) {
         int left=0;
         int right=0;
         int sum=0;
         int min=Integer.MAX_VALUE;
         for(;right<nums.length;){
             //将窗口中的元素添加到sum中
             sum+=nums[right];
             //判断sum是否大于等于target
             while(sum>=target){
                //将num[left]的值从sum从sum中减去
                sum-=nums[left];
                 //更新最小长度
                 min=Math.min(min,right-left+1);
                 //左边界右移
                 left++;
             }
             //扩大右边界
             right++;
         }
         //判断是否min大小,若是Integer.MAX_VALUE则返回0
         return min==Integer.MAX_VALUE?0:min;
     }
}

无重复字符的最长子串

算法分析

 题目要求找不重复的最长子串的长度,如果直接暴力枚举出每一个子串,时间复杂度会达到O(n^2),这样的方法,可能会超时,所以,这道题我们可以使用滑动窗口还有哈希表来解决。

算法步骤

  1. 初始化变量:定义双指针left和right,并初始化为0。设置一个hash数组,用来模拟哈希表,【hash数组的大小根据ASCII码表,一共有128个字符,所以我们可以设置为128】。定义ans用来存储最长不重复子串的长度,并初始化为0.
  2. 右边界(right)移动:将right往右移动,在移动的过程中,每移动一次,就将ch[right]在hash中以ch[right]为下标的元素加1。
  3. 更新ans值:在右边界移动的过程中,每次都将ans的值进行更新。
  4. 处理重复字符:当right移动到某个位置之后,要将ch[right]在hash对应的位置元素+1,需要先判断当前位置元素是否大于1,若大于1,说明子串遇到了重复的字符,此时需要移动left指针,并将在hash中以ch[left]为下标的元素-1,同时left右移,直到将重复元素排除。
  5. 遍历结束:当right遍历完字符串,结束循环,并返回ans。

时间复杂度:O(n),n是字符串长度,在入窗口的时候,right遍历一遍,left在最坏情况下也只遍历一遍。

空间复杂度:O(1),用的都是固定大小的变量。

示例

以{“abcabcbb”}为例

第一步:初始化

  • int left=0,right=0
  • hash[] = new int[128];
  • int ans=0;
  • char ch={'a','b','c','a','b','c','b','b'};

第二、三步:右边界right右移,并且在right往右走的过程中,更新ans的值

 第四步:处理重复字符

在上图中,第5个图,就是进行去重的情况。

第五步:重复上述操作,直到right到字符串末尾

算法代码

 /**
      * 计算给定字符串中最长无重复字符的子串的长度。
      * 通过维护一个滑动窗口,使用数组hash来记录窗口内每个字符出现的次数。
      * 当遇到重复字符时,缩小窗口的左边界,直到重复的字符不再出现在窗口内。
      * 在每次移动窗口右边界时,更新最长无重复子串的长度。
      *
      * @param s 输入的字符串
      * @return 最长无重复字符子串的长度
      */
     public int lengthOfLongestSubstring(String s) {
         // 初始化最长长度为0
         int ans = 0;
         // 初始化窗口的左右边界
         int left = 0;
         int right = 0;
         // 将字符串转换为字符数组,方便操作
         char[] ch = s.toCharArray();
         // 创建一个数组作为简化版的哈希表,用于记录字符出现的次数
         int[] hash = new int[128];

         // 遍历字符数组
         for (; right < ch.length; right++) {
             // 将当前字符的出现次数加1
             hash[ch[right]]++;
             // 当当前字符出现次数大于1时,说明遇到了重复字符
             while (hash[ch[right]] > 1) {
                 // 将窗口左边界右移,并将左边界字符的出现次数减1
                 hash[ch[left++]]--;
             }
             // 更新最长无重复字符子串的长度
             // 更新ans
             ans = Math.max(ans, right - left + 1);
         }
         // 返回最长无重复字符子串的长度
         return ans;
     }

最大连续1的个数 III

算法分析

这道题给定了一个只有0和1的数组,且给定允许改变数组中k个0,通过改变数组中k个0来得到一个最长连续1。如果我们使用暴力枚举的话,时间复杂度会达到O(n^2),可能会超时。我们可以使用滑动窗口以及给其外加一个计零器来解决。

算法步骤

  1. 初始化:定义两个指针leftright并初始化为0。设置一个zero作为计零器并初始化为0.设置一个ans并初始化为0,用来记录最长1的长度。
  2. 扩大窗口:右边界right往右移,并判断nums[right]是不是0,若是0,则zero++。
  3. 左边界右移:当zero大于k时,说明此时在窗口内的0大于k,要进行出窗口,直到zero=k。
  4. 更新ans:在right往右移的过程中,每次都需要更新ans值。
  5. 循环操作重复2-3-4操作
  6. 遍历结束:直到right走到数组末尾,结束遍历,返回ans。

时间复杂度:0(n),n是数组长度,在整个过程中,进窗口的时候,right遍历一遍数组,出窗口的时候,最坏的情况下,left遍历一遍数组。

空间复杂度:0(1),在算法中,只用到了几个变量。

示例

[1,1,1,0,0,0,1,1,1,1,0],K=2为例,

第一步:初始化

  • left=0,right=0;
  • zero=0,ans=0;

第二步:右边界移动

让right往右移

第三步:左边界右移,直到zero=k

 

第四步:重复上述操作

第五步:当right遍历完数组,此时返回结果即可。由图可知,最长连续1的个数为6.

算法代码

class Solution {
   /**
 * 在给定数组中找到最长的子数组,该子数组中的0和1的个数之和不超过k。
 * 该方法通过滑动窗口算法实现,避免了重复计算,提高了效率。
 * 
 * @param nums 输入的整数数组,其中包含0和1。
 * @param k 允许的0和1的个数之和的最大值。
 * @return 返回满足条件的最长子数组的长度。
 */
public int longestOnes(int[] nums, int k) {
    /* 初始化最长子数组的长度为0 */
    int ans = 0;
    /* 初始化滑动窗口的左边界 */
    int left = 0;
    /* 初始化滑动窗口的右边界 */
    int right = 0;
    /* 初始化窗口中0的个数 */
    int zero = 0;
    /* 遍历数组,移动右边界 */
    for (; right < nums.length; right++) {
        /* 如果当前元素为0,增加窗口中0的个数 */
        if (nums[right] == 0) {
            zero++;
            /* 如果窗口中0的个数超过了k,移动左边界,减少窗口中0的个数 */
            while (zero > k) {
                if (nums[left++] == 0) {
                    zero--;
                }
            }
        }
        /* 更新最长子数组的长度 */
        //更新ans值
        ans = Math.max(ans, right - left + 1);
    }
    /* 返回最长子数组的长度 */
    return ans;
}
}

 将 x 减到 0 的最小操作数

算法分析

这道题如果我们按着它所给题意来做的话,是很困难的。题意就是想要让我们在数组两边进行操作,然后找两侧的数与x抵消之后让x为0。所以,我们可以采用正难则反的思想,既然是要求两侧的数之和能与x抵消,那么我们可以想象成就是要就是要在数组中找除了x之外,其他数之和的长度。最后让数组长度(len)减去其他数之和的长度,最终得到的就是我们想要能让x减为0的长度。

算法步骤

  1. 初始化:定义两个指针leftright,表示滑动窗口的两个边界。定义sum并初始化为0,用于记录滑动窗口中元素之和;定义target并初始化为0,用于后续记录数组中除了x之外其他数之和;定义ans并初始化为-1,用于数组中是否存在能让x为0的数,若没有则返回-1。
  2. 判断target:先将数组中所有的数加到target中,再让target减去x,并且判断此时target是否大于0,若小于0,说明数组中所有数之和比x要小,则直接返回-1.
  3. 扩大窗口:让right往右移动,并将nums[right]添加到sum中。
  4. 判断sum与target:若sum的值比target要大,说明此时需要缩小窗口,让sum减去nums[left]的值,并让left往右移动,直到sum不再大于target。
  5. 更新ans值:当sum的值与target相等,说明找到了一个符合条件的子数组,比较ans和right-left+1的大小,存放最大值在ans中。
  6. 重复:重复3、4、5操作,直到right走到数组末尾位置。
  7. 返回ans值:在返回值时,需要进行判断ans的大小,若ans=-1,说明数组不存在能让x为0的操作数;反之,则返回nums.length-ans的长度。

时间复杂度:O(n),n为数组长度,在入窗口的时候,right遍历一遍数组,在出窗口的时候,最坏情况下遍历一遍数组。

空间复杂度:O(1),只用了几个变量。

示例

 以[1,1,4,2,3],x=5为例

第一、二步:初始化,并将数组中所有元素添加到target,最后让target-x,判断target大小

target=1+1+4+2+3=11

target-5=6

第三、四步:扩大窗口,判断target与sum的大小

让right往右移动,并将nums[right]添加到sum中

第五步:更新ans值

在上图中,当right走到以2为下表的数时,此时sum与target相等,更新ans值

ans=Math.max(ans,right-left+1).,此时ans=3

第六步:重复上述操作。

第七步:返回ans值,当right遍历完数组之后,返回ans=3. 

算法代码

/**
 * 计算最少操作次数,使得数组nums中的元素之和减去x后为0。
 * 操作包括将数组中的任意一个元素翻倍或减半。
 * 
 * @param nums 输入的整数数组
 * @param x 需要减去的目标值
 * @return 返回最少操作次数,如果无法实现则返回-1
 */
public int minOperations(int[] nums, int x) {
    // 初始化答案为-1,表示未找到符合条件的解
    int ans = -1;
    // 初始化左右指针和当前窗口内元素之和
    int left = 0;
    int right = 0;
    int sum = 0;
    // 计算目标值,即数组元素之和减去x
    int target = 0;
    for (int digit : nums) {
        target += digit;
    }
    target -= x;
    // 如果目标值小于0,说明无法通过操作达到目标,直接返回-1
    if (target < 0) return ans;
    // 遍历数组,寻找符合条件的最长子数组
    while (right < nums.length) {
        sum += nums[right];
        // 如果当前窗口内元素之和大于目标值,移动左指针缩小窗口
        while (sum > target) {
            sum -= nums[left++];
        }
        // 如果当前窗口内元素之和等于目标值,更新答案
        if (sum == target) {
            ans = Math.max(ans, right - left + 1);
        }
        right++;
    }
    // 根据答案计算并返回最终结果,如果答案为-1,则返回数组长度
    return ans == -1 ? -1 : nums.length - ans;
}

以上就是本篇所有内容,若有不足欢迎指正~

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
滑动窗口算法(Sliding Window Algorithm)是一种常用的算法技巧,用于解决一些数组或字符串相关的问题。它通过维护一个固定大小的窗口,并在窗口滑动的过程中对窗口中的元素进行处理,从而得到问题的解。 下面是一个使用滑动窗口算法解决问题的示例,使用Java语言实现: ```java public class SlidingWindowAlgorithm { public static void main(String[] args) { int[] nums = {2, 4, 1, 5, 3, 2, 7, 1}; int targetSum = 8; int result = findTargetSum(nums, targetSum); System.out.println("Result: " + result); } public static int findTargetSum(int[] nums, int targetSum) { int windowSum = 0; // 窗口内的元素和 int windowStart = 0; // 窗口的起始位置 int minLength = Integer.MAX_VALUE; // 记录最小子数组长度 for (int windowEnd = 0; windowEnd < nums.length; windowEnd++) { // 窗口右移,加上当前元素 windowSum += nums[windowEnd]; // 当窗口内元素和大于等于目标和时,缩小窗口 while (windowSum >= targetSum) { // 更新最小子数组长度 minLength = Math.min(minLength, windowEnd - windowStart + 1); // 窗口左移,减去左边界元素 windowSum -= nums[windowStart]; windowStart++; } } return minLength != Integer.MAX_VALUE ? minLength : 0; } } ``` 以上示例中,我们使用滑动窗口算法来寻找数组中和大于等于目标和的最小子数组长度。通过维护一个窗口,不断向右移动,并根据窗口内元素和与目标和的比较来调整窗口的大小。最后返回最小子数组长度。 希望以上示例能帮助到您理解滑动窗口算法的使用。如果您有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhyhgx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值