您的位置:首页 > 教育 > 培训 > leetcode滑动窗口问题

leetcode滑动窗口问题

2024/10/6 4:07:19 来源:https://blog.csdn.net/m0_73883551/article/details/141352060  浏览:    关键词:leetcode滑动窗口问题

想成功先发疯,不顾一切向前冲。

第一种

定长滑动窗口

. - 力扣(LeetCode)1456.定长子串中的元音的最大数目. - 力扣(LeetCode)

No.1

定长滑窗套路
我总结成三步:入-更新-出。

        1. 入:下标为 i 的元素进入窗口,更新相关统计量。如果 i<k−1 则重复第一步。
        2. 更新:更新答案。一般是更新最大值/最小值。        
        3. 出:下标为 i−k+1 的元素离开窗口,更新相关统计量。
以上三步适用于所有定长滑窗题目。

class Solution {public int maxVowels(String S, int k) {char[] s = S.toCharArray();int ans = 0;int temp = 0;for (int i = 0; i < s.length; i++) {// 1. 进入窗口if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' || s[i] == 'u') {temp++;}if (i < k - 1) { // 窗口大小不足 kcontinue;}// 2. 更新答案ans = Math.max(ans, temp);// 3. 离开窗口char out = s[i - k + 1];if (out == 'a' || out == 'e' || out == 'i' || out == 'o' || out == 'u') {temp--;}}return ans;}
}
复杂度分析
  • 时间复杂度:O(n),其中 n 是 s 的长度。
  • 空间复杂度:O(1)。仅用到若干额外变量。

这里对于字符串的处理及java的String类做一个详细说明

toCharArray()

是 Java 中 String 类的一个方法,用于将字符串转换为字符数组 (char[])。每个字符数组中的元素对应于字符串中的一个字符。

用法

String str = "Hello, World!"; char[] charArray = str.toCharArray();

解释

  • str 是一个 String 对象。
  • 调用 str.toCharArray() 会将字符串 "Hello, World!" 转换为一个字符数组。
  • charArray 将包含:{'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'}

常见用途

  • 字符串处理:你可以更方便地遍历字符串中的每个字符。
  • 字符修改:字符数组可以修改单个字符,而 String 是不可变的(即一旦创建,不能更改其内容)。

    1. length()

  • 作用: 返回字符串的长度(字符数)。
    • 示例:
      String str = "Hello, World!"; 
      int len = str.length(); // len = 13 
  • 2. charAt(int index)

  • 作用: 返回指定索引处的字符,索引从 0 开始。
  • 示例:
    String str = "Hello"; 
    char ch = str.charAt(1); // ch = 'e'

  • 3. substring(int beginIndex) / substring(int beginIndex, int endIndex)

  • 作用: 返回从指定索引开始或在索引区间内的子字符串。
  • 示例:
    String str = "Hello, World!"; 
    String subStr1 = str.substring(7); // subStr1 = "World!" 
    String subStr2 = str.substring(0, 5); // subStr2 = "Hello" 

  • 4. indexOf(String str) / indexOf(char ch)

  • 作用: 返回指定字符或子字符串在原字符串中第一次出现的索引,若未找到则返回 -1。
  • 示例:
    String str = "Hello, World!"; 
    int index1 = str.indexOf('W'); // index1 = 7 
    int index2 = str.indexOf("World"); // index2 = 7 

  • 5. toUpperCase() / toLowerCase()

  • 作用: 返回将字符串转换为大写或小写后的新字符串。
  • 示例:
    String str = "Hello"; 
    String upperStr = str.toUpperCase(); // upperStr = "HELLO" 
    String lowerStr = str.toLowerCase(); // lowerStr = "hello" 

  • 6. trim()

  • 作用: 去除字符串开头和结尾的空白字符。
  • 示例:
    String str = " Hello, World! "; 
    String trimmedStr = str.trim(); // trimmedStr = "Hello, World!" 

  • 7. replace(char oldChar, char newChar) / replace(CharSequence target, CharSequence replacement)

  • 作用: 替换字符串中的指定字符或子字符串。
  • 示例:
    String str = "Hello, World!"; 
    String replacedStr1 = str.replace('o', 'a'); // replacedStr1 = "Hella, Warld!" 
    String replacedStr2 = str.replace("World", "Java"); // replacedStr2 = "Hello, Java!" 

  • 8. equals(Object anObject) / equalsIgnoreCase(String anotherString)

  • 作用: 比较两个字符串的内容是否相等。equalsIgnoreCase 不区分大小写
  • 示例:
    String str1 = "Hello"; 
    String str2 = "hello"; 
    boolean isEqual = str1.equals(str2); // isEqual = false 
    boolean isEqualIgnoreCase = str1.equalsIgnoreCase(str2); // isEqualIgnoreCase = true

  • 9. split(String regex)

  • 作用: 根据正则表达式将字符串分割为子字符串数组。
  • 示例:
    String str = "apple,banana,orange"; 
    String[] fruits = str.split(","); // fruits = ["apple", "banana", "orange"] 

  • 10. contains(CharSequence s)

  • 作用: 判断字符串是否包含指定的字符序列。
  • 示例:
    String str = "Hello, World!"; 
    boolean containsHello = str.contains("Hello"); // containsHello = true

No.2

给你一个 下标从 0 开始 的整数数组 nums ,其中 nums[i] 表示第 i 名学生的分数。另给你一个整数 k 。

从数组中选出任意 k 名学生的分数,使这 k 个分数间 最高分 和 最低分 的 差值 达到 最小化 。

返回可能的 最小差值 。

示例 1:

输入:nums = [90], k = 1
输出:0
解释:选出 1 名学生的分数,仅有 1 种方法:
- [90] 最高分和最低分之间的差值是 90 - 90 = 0
可能的最小差值是 0

示例 2:

输入:nums = [9,4,1,7], k = 2
输出:2
解释:选出 2 名学生的分数,有 6 种方法:
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 4 = 5
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 1 = 8
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 7 = 2
- [9,4,1,7] 最高分和最低分之间的差值是 4 - 1 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 4 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 1 = 6
可能的最小差值是 2
class Solution {public int minimumDifference(int[] nums, int k) {Arrays.sort(nums);int len = nums.length;int res = Integer.MAX_VALUE;for (int i = 0; i <= len - k; i++) {res = Math.min(res,nums[i+k-1]-nums[i]);}return res;}
}

这个题要注意的地方是:i  只有在 <= len-k 的条件下可以运行,在后续的nums[ ]读数的时候左右节点要进行移动读数;

No.3

1343. - 力扣(LeetCode)

class Solution {public int numOfSubarrays(int[] arr, int k, int threshold) {int len = arr.length;int ans = 0;for (int i = 0; i < k; i++) {ans += arr[i];}int res = 0;
//很典型的一个坑,初次忘记考虑当 i = 0 取 k 个数的时候的 ans 的大小的判断if(ans>=k*threshold){res++;}for (int i = k; i < len; i++) {ans = ans-arr[i-k] +arr[i];if(ans/k>=threshold){res++;}}return res;}
}

不定长滑动窗口(求最长或最大)

No.1

3.无重复字符的最长子串

我认为思考的重点在于滑动窗口的起始位置与一般规律的总结:

我们不妨以示例一中的字符串 abcabcbb 为例,找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:

以 (a)bcabcbb 开始的最长字符串为 (abc)abcbb;
以 a(b)cabcbb 开始的最长字符串为 a(bca)bcbb;
以 ab(c)abcbb 开始的最长字符串为 ab(cab)cbb;
以 abc(a)bcbb 开始的最长字符串为 abc(abc)bb;
以 abca(b)cbb 开始的最长字符串为 abca(bc)bb;
以 abcab(c)bb 开始的最长字符串为 abcab(cb)b;
以 abcabc(b)b 开始的最长字符串为 abcabc(b)b;
以 abcabcb(b) 开始的最长字符串为 abcabcb(b)。

class Solution {public int lengthOfLongestSubstring(String S) {int len = S.length();char[] s = S.toCharArray();Set<Character> hs = new HashSet<>();int res = 0;for (int left = 0, right = 0; right < len; right++) {char ch = s[right];while (hs.contains(ch)) {hs.remove(s[left]);left++;}hs.add(s[right]);res = Math.max(res, right - left + 1);}return res;}
}
  • right: 当前子串的右边界索引。
  • left: 当前子串的左边界索引。

子串的长度计算公式是 右边界索引 - 左边界索引 + 1寓意着包含左右边界的字符串

No.2

1695.删除子数组的最大得分. - 力扣(LeetCode)

给你一个正整数数组 nums ,请你从中删除一个含有 若干不同元素 的子数组删除子数组的 分 就是子数组各元素之 和 。

返回 只删除一个 子数组可获得的 最大得分 。

如果数组 b 是数组 a 的一个连续子序列,即如果它等于 a[l],a[l+1],...,a[r] ,那么它就是 a 的一个子数组。

示例 1:

输入:nums = [4,2,4,5,6]
输出:17
解释:最优子数组是 [2,4,5,6]

示例 2:

输入:nums = [5,2,1,2,5,2,1,2,5]
输出:8
解释:最优子数组是 [5,2,1] 或 [1,2,5]
class Solution {public int maximumUniqueSubarray(int[] nums) {int len = nums.length; // 获取数组的长度Set<Integer> hs = new HashSet<>(); // 创建一个 HashSet 用来存储当前子数组的元素int res = 0, ans = 0; // res 用于存储当前子数组的元素和,ans 用于存储最大得分int i = 0; // i 指针,表示当前子数组的起点// j 指针表示当前子数组的终点for (int j = 0; j < len; j++) {// 当 hs 中已经包含 nums[j] 时,说明存在重复元素,需要移动 i 指针while (hs.contains(nums[j])) {hs.remove(nums[i]); // 从 HashSet 中移除子数组的第一个元素res -= nums[i]; // 从当前子数组的和中减去该元素的值i++; // 移动起点指针 i}hs.add(nums[j]); // 将 nums[j] 添加到 HashSet 中res += nums[j]; // 将 nums[j] 的值加到当前子数组的和中ans = Math.max(res, ans); // 更新最大得分}return ans; // 返回只删除一个子数组可以获得的最大得分}
}

No.3 上难度

1004.最大连续1的个数III. - 力扣(LeetCode)

给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。

示例 1:

输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:

输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

这个题有个特点就是数组内只有 0 ,1

那我们就围绕这一特点进行攻破,及,cnt += 1 - nums[right];  // 如果 nums[right] 是 0,`cnt` 增加1;如果是 1,`cnt` 不变

class Solution {public int longestOnes(int[] nums, int k) {int cnt = 0;  // 记录当前窗口内0的个数int ans = 0;  // 记录最长的全1子数组的长度int left = 0; // 滑动窗口的左边界// 右边界 `right` 从 0 开始遍历整个数组for (int right = 0; right < nums.length; right++) {cnt += 1 - nums[right]; // 如果 nums[right] 是 0,`cnt` 增加1;如果是 1,`cnt` 不变// 如果当前窗口内的0的个数超过了允许的k个,则需要收缩窗口while (cnt > k) {cnt -= 1 - nums[left]; // 移动左边界,移除 `nums[left]` 的影响left++; // 将左边界右移}// 更新最长子数组的长度ans = Math.max(ans, right - left + 1);}return ans; // 返回找到的最长的全1子数组的长度}
}
class Solution {public int longestOnes(int[] nums, int k) {int len = nums.length;int l = 0, r = 0;while(r<len){if(nums[r++]==0) k--;if(k<0 && nums[l++]==0) k++;}return r-l;}
}

No.4 方法一

为1的最长子数组. - 力扣(LeetCode)

给你一个二进制数组 nums ,你需要从中删掉一个元素。
(注意:题目要求必须删除一个元素。也就说如果数组中全为 1,也要删除一个 1。换句话说,如果数组中不超过一个 0,长度都为原数组长度减1:

只包含一个0,将这个0删掉;
不包含0,随便删一个元素;)

请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。

如果不存在这样的子数组,请返回 0 。

提示 1:

输入:nums = [1,1,0,1]
输出:3
解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1 。

示例 2:

输入:nums = [0,1,1,1,0,1,1,0,1]
输出:5
解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1] 。

示例 3:

输入:nums = [1,1,1]
输出:2
解释:你必须要删除一个元素。

做个简单的分析:

这道题要求删除一个元素得到最长连续 1 的子数组,那么肯定希望把那些夹在 1 中间的 0 删掉。也就是说我们尝试删除每个 0 来看可以构成的最长连续 1 子数组,而能够成最长连续 1,一定是夹在两个 0 之间的范围。

那么这道题的思路就和 1004. 最大连续1的个数 III。后者是替换 k 个 0,参考 滑动窗口+索引优化:两个0之间有k个零, 我们的做法是找到包含 k 个 0 的最长区间 (left, right),将里面的 0 都替换成 1,长度即为 right - left - 1。

而这道题要找的是包含 1 个 0 的最长区间 (left, right),将里面的 0 删除。长度就是原来的长度 right - left - 1 再减 1,即 right - left - 2。实际上就是在 1004 题的方法上取 k = 1【找到包含 1 个 0 的区间】,那么长度的计算方式多减了 1。

class Solution {public int longestSubarray(int[] nums) {List<Integer> zeroIdxs = new ArrayList<>();    // 记录数组中0元素的索引int n = nums.length;zeroIdxs.add(-1);   // -1表示左边界for(int i = 0; i < n; i++){if(nums[i] == 0)zeroIdxs.add(i);}zeroIdxs.add(n);   // n表示右边界if(zeroIdxs.size() <= 3) return n - 1;   // 数组中0的个数不超过1个,直接删除唯一0或任意元素,整个数组都为连续1int m = zeroIdxs.size();int res = 0;for(int i = 2; i < m; i++){// 枚举每个滑动窗口右边界i,左边界就为i - 2,保证(i,j)之间有1个0// (i, j)本来有j - i - 1个数,还要删掉一个0,长度即为 j - i - 2res = Math.max(res, zeroIdxs.get(i) - zeroIdxs.get(i - 2) - 2);}return res;  }
}

No.4 推荐方法二

左右指针+贪吃蛇思路,胃口大小为k=1,遇到0则吃,胃口饱后每前移一位就拉一位,若拉出的是0则恢复1胃口,最后算长度

class Solution {public int longestSubarray(int[] nums) {int len = nums.length; // 获取数组的长度int l = 0, r = 0, k = 1; // 初始化左右指针l和r,以及可删除的0的数量k// 遍历数组while (r < len) {if (nums[r++] == 0) k--; // 如果右指针所指元素是0,则k减1,同时右指针右移if (k < 0 && nums[l++] == 0) k++; // 如果k小于0,且左指针所指元素是0,则k加1,同时左指针右移}return r - l - 1; // 返回最长子数组长度,-1是因为需要删除一个元素}
}
  1. 初始化lr 是左右指针,用于维护一个滑动窗口,k 是允许删除的 0 的数量,初始值为 1,因为我们允许删除一个 0

  2. 遍历数组:用 r 指针遍历整个数组,每次遇到 0,我们将 k 减 1。当 k 变为负数时,说明我们已经删除了超过允许数量的 0,这时就需要移动 l 指针来缩小窗口。

  3. 调整窗口:当 k 小于 0 时,说明窗口内有多余的 0 需要移除,因此我们移动 l 指针,直到 k 恢复到非负状态。

  4. 返回结果:最终,r - l - 1 计算的是最大子数组的长度,减 1 是因为题目要求删除一个元素。

No.5  巩固一下

. - 力扣(LeetCode)

class Solution {public int longestSemiRepetitiveSubstring(String s) {int ans = 1; // 初始化结果,表示最长半重复子串的长度int left = 0; // 左指针,表示当前子串的起始位置int same = 0; // 记录当前重复字符的对数int n = s.length(); // 获取字符串的长度// 右指针从位置1开始遍历字符串for (int right = 1; right < n; right++) {// 如果当前字符与前一个字符相同if (s.charAt(right) == s.charAt(right - 1)) {same++; // 增加重复字符对的计数// 如果重复字符对超过1个if (same > 1) {// 移动左指针,直到找到下一个重复字符对的起点left++;while (s.charAt(left) != s.charAt(left - 1)) {left++;}same = 1; // 重置重复字符对的计数}}// 更新最长半重复子串的长度ans = Math.max(ans, right - left + 1);}return ans; // 返回最长半重复子串的长度}
}

不定长滑动窗口(求最短/最小)

No.1

209.长度最小的子数组. - 力扣(LeetCode)

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 

子数组

 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
class Solution {public int minSubArrayLen(int target, int[] nums) {int n = nums.length;  // 获取数组的长度int ans = n + 1;      // 初始化结果为 n + 1,表示一个不可能达到的最大长度(为了后面比较)int sum = 0;          // 当前子数组的和int left = 0;         // 子数组的左边界起始为0// 使用滑动窗口方法遍历数组for (int right = 0; right < n; right++) {sum += nums[right];  // 将右边界的元素加到当前子数组的和中// 当当前子数组的和大于等于目标值时,尝试缩小窗口while (sum >= target) { // 更新答案为当前子数组长度的最小值ans = Math.min(ans, right - left + 1);// 从左边界开始缩小窗口sum -= nums[left++]; }}// 如果找到了有效的子数组,返回最小长度;否则返回0return ans <= n ? ans : 0;}
}

解释:

变量 ans 用于存储当前找到的满足条件的最小子数组长度。代码执行时,它初始化为 n + 1(比数组最大可能长度大1的一个值),这个初始值用来保证在后续计算中,如果找不到满足条件的子数组,结果能被正确判断为无效(因为不可能有子数组长度大于数组本身的长度)。

ans 的更新逻辑如下:

  • 每当子数组的和 sum 大于或等于目标值 target 时,当前子数组的长度 right - left + 1 可能是一个新的候选解。
  • 通过 ans = Math.min(ans, right - left + 1) 语句,代码在找到新的更短的子数组时更新 ans
  • 最后,ans 只有在它小于等于 n 时才会被返回,表示找到了一组符合条件的子数组,否则返回 0,表示没有找到这样的子数组。


 

不定长滑动窗口(求子数组个数)

No.1

713.乘积小于K的子数组. - 力扣(LeetCode)

class Solution {public int numSubarrayProductLessThanK(int[] nums, int k) {// 如果 k 小于或等于 1,不可能有乘积小于 k 的子数组if (k <= 1) return 0;int len = nums.length;int left = 0;    // 滑动窗口的左边界int product = 1; // 当前窗口内元素的乘积int count = 0;   // 满足条件的子数组数量// 遍历数组的每个元素,作为窗口的右边界for (int right = 0; right < len; right++) {product *= nums[right]; // 乘上当前元素// 如果乘积大于等于 k,缩小窗口的左边界,直到乘积小于 kwhile (product >= k) {product /= nums[left]; // 移除左边界元素的影响left++;                // 左边界右移}// 统计以 nums[right] 结尾的、乘积小于 k 的子数组数量count += right - left + 1;}return count; // 返回总的满足条件的子数组数量}
}

还算好理解,咱们一起看下一道菜:

多指针滑动窗口

No.1

. - 力扣(LeetCode)2563.统计公平数对的数目

给你一个下标从 0 开始、长度为 n 的整数数组 nums ,和两个整数 lower 和 upper ,返回 公平数对的数目 。

如果 (i, j) 数对满足以下情况,则认为它是一个 公平数对 :

  • 0 <= i < j < n,且
  • lower <= nums[i] + nums[j] <= upper

示例 1:

输入:nums = [0,1,7,4,4,5], lower = 3, upper = 6
输出:6
解释:共计 6 个公平数对:(0,3)、(0,4)、(0,5)、(1,3)、(1,4) 和 (1,5) 。

示例 2:

输入:nums = [1,7,9,2,5], lower = 11, upper = 11
输出:1
解释:只有单个公平数对:(2,3) 。
class Solution {public long countFairPairs(int[] nums, int lower, int upper) {Arrays.sort(nums);int left = nums.length, right = left;long ans = 0;for (int i = 0; i < nums.length; i++) {while (right > 0 && nums[right - 1] > upper-nums[i]) {right--;}while (left > 0 && nums[left - 1] >= lower-nums[i]) {left--;}ans += Math.min(right, i) - Math.min(left, i);}return ans;}
}

完结

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com