算法学习(5):LeetCode刷题之滑动窗口

本文详细介绍了滑动窗口算法在四个经典LeetCode问题的应用:查找异位词子串、找无重复字符最长子串、连续1的最大个数及长度最小满足条件子数组。通过实例展示了如何利用固定和不固定窗口大小的方法解决这些问题。
摘要由CSDN通过智能技术生成

前言:

滑动窗口算法专门优化一种连续问题场景,如找出字符串或者数组中满足xx条件的最长(或最短)的连续子串(或子数组)。

滑动窗口的解题思路如下:
需要用到双指针进行求解,两个指针构造一个窗口,窗口的移动是重点!
右指针每次往前移动一格,每次移动会有一个新的元素进入窗口,这时条件可能就会发生变化,再根据当前条件来决定左指针是否移动,以及移动多少格。总的来说,右指针每次必然要移动一格,目的是要探索“可能性”;左指针要随着移动,目的是要确定这次探索的“正确性”。滑动窗口的时间复杂度是O(n),因为2个指针分别遍历一次数组。

滑动窗口一般有3种类型:
1、固定窗口大小;
2、窗口大小不固定,求解最大能满足条件的窗口;
3、窗口大小不固定,求解最小能满足条件的窗口;
但是不管是哪种类型,基本的思路都是一样的,不一样的仅仅是代码的细节。

固定窗口大小的模板比较简单:

public int slidingWindowTemplate(String[] a, ...) {
    // 输入参数有效性判断
    if (...) {
        ...
    }

    // 申请一个散列,用于记录窗口中具体元素的个数情况
    // 这里用数组的形式呈现,也可以考虑其他数据结构,如Set,Map等
    int[] hash = new int[...];

    // 预处理(可省), 一般情况是改变 hash
    ...

    // l 表示左指针,r 表示右指针,如果是固定窗口,也可以只使用一个变量i来控制窗口大小
    // count 记录当前的条件,具体根据题目要求来定义
    // result 用来存放结果
    int l = 0, r = 0, count = ..., result = ...;
    for (int i = 0; i < ...; i++) {
    	// 先找到窗口的后边界
    }
    
	for (int i = 右边界; i < ...; i++) {
    	// 再依次滑动窗口,判断是否满足条件
    	if (条件满足) {
			// 更新结果
			results = ...
		}
    }

    return results;
}

不固定窗口大小,代码模板如下:

public int slidingWindowTemplate(String[] a, ...) {
    // 输入参数有效性判断
    if (...) {
        ...
    }

    // 申请一个散列,用于记录窗口中具体元素的个数情况
    // 这里用数组的形式呈现,也可以考虑其他数据结构,如Set,Map等
    int[] hash = new int[...];

    // 预处理(可省), 一般情况是改变 hash
    ...

    // l 表示左指针,r 表示右指针
    // count 记录当前的条件,具体根据题目要求来定义
    // result 用来存放结果
    int l = 0, r = 0, count = ..., result = ...;
    while (r < A.length) {
        // 更新新元素在散列中的数量
        hash[A[r]]--;

        // 根据窗口的变更结果来改变条件值
        if (hash[A[r]] == ...) {
            count++;
        }

        // 如果当前条件不满足,移动左指针直至条件满足为止
        while (count > K || ...) {
            ...
            if (...) {
                count--;
            }
            hash[A[l]]++;
            l++;
        }

        // 更新结果
        results = ...
    }

    return results;
}

正文

1、LeetCode 438. 找到字符串中所有字母的异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

这是一道固定窗口大小的滑动窗口题,窗口的大小就是字符串p的长度。我们先构造好窗口,再把窗口一点一点往右滑动,滑动的过程中判断条件是否成立。代码如下:

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int sl = s.length();
        int pl = p.length();
        List<Integer> ans = new ArrayList<>();
        if (sl < pl) {
            return ans;
        }
		// 由于题目中说明了字符串只包含小写字母,所以可以使用数组进行字母计数
        int[] sArr = new int[26];
        int[] pArr = new int[26];
        // 先构造出初始化的窗口
        for (int i = 0; i < pl; i++) {
            sArr[s.charAt(i) - 'a']++;
            pArr[p.charAt(i) - 'a']++;
        }
        if (Arrays.equals(sArr, pArr)) {
            ans.add(0);
        }
		// 再一点一点滑动窗口,注意判断满足的条件
        for (int i = pl; i < sl; i++) {
            sArr[s.charAt(i) - 'a']++;
            sArr[s.charAt(i - pl) - 'a']--;
            if (Arrays.equals(sArr, pArr)) {
                ans.add(i - pl + 1);
            }
        }
        return ans;
    }
}

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

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

示例 1:
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3

这是一道窗口大小变化的滑动窗口题,先移动右指针,每次将窗口里增加一个元素,增加后,再判断是否满足条件,不满足的话,移动左指针,直到满足条件。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int left = 0;
        int right = 0;

        int ans = 0;
        // 题目要求无重复的元素,可以使用Set结构进行存储
        Set<Character> set = new HashSet<>();
        // 移动右指针
        while (right < s.length()) {
            char c = s.charAt(right);
            right++;
            // 如果不再满足条件,移动左指针,直到条件再次满足
            // 滑动窗口题目最重要的点就是要找到满足窗口的条件
            while (set.contains(c)) {
                set.remove(s.charAt(left));
                left++;
            }
            set.add(c);
            // 更新结果
            ans = Math.max(ans, set.size());
        }
        return ans;
    }
}

3、LeetCode 1004. 最大连续1的个数 III

给定一个由若干 01 组成的数组 A,我们最多可以将 K 个值从 0 变成 1 。返回仅包含 1 的最长(连续)子数组的长度。

示例 1:
输入:A = [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

如果这道题没有说可以从0变成1,那么将非常简单,如果加了这个条件,难点在于不知道要将哪些0变成1,如果使用枚举,复杂度将非常大。这道题同样可以使用滑动窗口,限定条件就是窗口内的0的个数不能多于K个。

class Solution {
    public int longestOnes(int[] nums, int k) {
        int left = 0;
        int right = 0;

        int ans = 0;
        int total = k;
        // 移动右指针
        while (right < nums.length) {
        	// 移动过程中统计0的个数
            if (nums[right] == 0) {
                total--;
            }
            right++;
            // 如果不再满足条件,即窗口中0的个数多于K个了,就要移动左指针,
            while (total < 0) {
                if (nums[left] == 0) {
                    total++;
                }
                left++;
            }
            // 更新结果
            ans = Math.max(ans, right - left);
        }
        return ans;
    }
}

4、LeetCode 209. 长度最小的子数组

给定一个含有 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,在条件满足的情况下,尽可能的移动右指针,这样才能找到最小的间隔,代码如下:

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int right = 0;

        int ret = Integer.MAX_VALUE;
        int tmpSum = 0;
        // 右移指针
        while (right < nums.length) {
        	// 累加窗口内元素之和
            tmpSum += nums[right];
            right++;
            // 在满足条件的情况下,尽可能收缩窗口
            while (tmpSum >= target) {
                ret = Math.min(ret, right - left);
                tmpSum -= nums[left];
                left++;
            }
        }
        return ret == Integer.MAX_VALUE ? 0 : ret;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值