【LeetCode】【数组】【滑动窗口相关问题】

本文探讨了滑动窗口在解决无重复字符最长子串、存在重复元素以及存在重复元素II和III等问题中的应用。通过使用滑动窗口和哈希集合,可以有效地求解这些问题,达到线性时间复杂度。滑动窗口的核心在于维护一个窗口,通过移动左右边界来满足特定条件。同时,文章还提到了桶排序方法在某些场景下的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

3. 无重复字符的最长子串——数组快很多因为不需要有set的存取操作

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

输入: s = “abcabcbb”
输出: 3

思路:滑动窗口
队列也好数组也好,其实就是一个控制左右的东东,右边满足条件就扩一下,左边不满足了就踢出去,然后注意边界条件!
time On
A1.unordered_set

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        unordered_set<char> st;//hashset作为滑动窗口
        int maxStr = 0;//res
        int left = 0;
        for(int i = 0; i < s.size(); i++){
        //注意是个while,如果是abcc他会一直把左边删到只剩c
            while (st.find(s[i]) != st.end()){//如果有不满足的
                st.erase(s[left]);//erase
                left ++;//维护左指针
            }
            maxStr = max(maxStr,i-left+1);//res要放外面,不能放while里,不然如果没有重复字符res就一直是0了
            st.insert(s[i]);//右边插入
    }
        return maxStr;
    }
};

A2.fre[256]

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        if(n==0) return 0;
        int fre[256] = {0};//妙就妙在这里,用一个fre记录出现的频次来决定右边怎么扩
        int l =0,r=-1,res=0;
        while(l<n){
            if(r+1<n&&fre[s[r+1]]==0){//注意边界条件
                fre[s[r+1]]++;
                r++;
            }
            else{
                fre[s[l]]--;
                l++;
            }
            res = max(res,r-l+1);
        }
        return res;
    }
};

219. 存在重复元素 II

给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。

输入: nums = [1,2,3,1], k = 3
输出: true
思路不能说和第三题差不多,只能说一模一样,这是固定窗口滑动,怎么实现是关键!

class Solution {
public:
    bool containsNearbyDuplicate(vector<int>& nums, int k) {
        unordered_set<int> st;
        for(int i =0;i<nums.size();i++){
            if(st.find(nums[i])!=st.end()){
                return true;
            }
            st.insert(nums[i]);
            if(st.size()>k){
                st.erase(nums[i-k]);
            }
        }
        return false;
    }
};

220. 存在重复元素 III

给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。
如果存在则返回 true,不存在返回 false。

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

思路
1.滑动窗口+set
就是上一题多了个条件,对应修改就好,有+法要考虑整形溢出问题所以要用long long

class Solution {
public:
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
        set<long long> st;//set是二叉树实现的Onlogn
        int left = 0;
        for(int right=0;right<nums.size();++right){
            // if(st.find(nums[i])!=st.end()){
            //     return true;
            // }
            //由|nums[i]-num[j]|<=t可以推出
			//nums[i]-t<=nums[j]<=nums[i]+t
            //set的内置函数lower_bound可以找到第一个大于等于nums[i]-t的数,则这个数再满足小于等于nums[i]+t即可
            auto a = st.lower_bound((long long)nums[right]-(long long)t);
            
            if(a!=st.end() && *a <= (long long)nums[right]+(long long)t){
                return true;
            }
            st.insert(nums[abs(*a - nums[right]) <= t]);
            if(st.size()>k){
                st.erase(nums[left]);
            }
        }
        return false;
    }
};

2.桶排序(还需打磨)
上述解法无法做到线性的原因是:我们需要在大小为 k 的滑动窗口所在的「有序集合」中找到与 u 接近的数。

如果我们能够将 k 个数字分到 kk 个桶的话,那么我们就能 O(1)O(1) 的复杂度确定是否有 [u - t, u + t][u−t,u+t] 的数字(检查目标桶是否有元素)。

具体的做法为:令桶的大小为 size = t + 1size=t+1,根据 u 计算所在桶编号:

如果已经存在该桶,说明前面已有 [u - t, u + t][u−t,u+t] 范围的数字,返回 true
如果不存在该桶,则检查相邻两个桶的元素是有 [u - t, u + t][u−t,u+t] 范围的数字,如有 返回 true
建立目标桶,并删除下标范围不在 [max(0, i - k), i)[max(0,i−k),i) 内的桶

#define LL long long
class Solution {
public:
    LL size;
    bool containsNearbyAlmostDuplicate(vector <int> & nums, int k, int t) {
        int n = nums.size();
        unordered_map<LL, LL> m;
        size = t + 1L;
        for (int i = 0; i < n; i++) {
            LL u = nums[i] * 1L;
            LL idx = getIdx(u);
            // 目标桶已存在(桶不为空),说明前面已有 [u - t, u + t] 范围的数字
            if (m.find(idx) != m.end()) return true;
            // 检查相邻的桶
            LL l = idx - 1, r = idx + 1;
            if (m.find(l) != m.end() && abs(u - m[l]) <= t) return true;
            if (m.find(r) != m.end() && abs(u - m[r]) <= t) return true;
            // 建立目标桶
            m.insert({idx, u});
            // 移除下标范围不在 [max(0, i - k), i) 内的桶
            if (i >= k) m.erase(getIdx(nums[i - k]));
        }
        return false;
    }
    LL getIdx(LL u) {
        return u >= 0 ? u / size : ((u + 1) / size) - 1;
    }
};
【重点】如何理解 getIdx() 的逻辑
为什么 size 需要对 t 进行 +1 操作?
目的是为了确保差值小于等于 t 的数能够落到一个桶中。

举个 🌰,假设 [0,1,2,3],t = 3,显然四个数都应该落在同一个桶。

如果不对 t 进行 +1 操作的话,那么 [0,1,2][3] 会被落到不同的桶中,那么为了解决这种错误,我们需要对 t 进行 +1 作为 size 。

这样我们的数轴就能被分割成:

0 1 2 3 | 4 5 6 7 | 8 9 10 11 | 12 13 14 15 | …

总结一下,令 size = t + 1 的本质是因为差值为 t 两个数在数轴上相隔距离为 t + 1,它们需要被落到同一个桶中。

当明确了 size 的大小之后,对于正数部分我们则有 idx = nums[i] / size。

如何理解负数部分的逻辑?
由于我们处理正数的时候,处理了数值 0,因此我们负数部分是从 -1 开始的。

还是我们上述 🌰,此时我们有 t = 3 和 size = t + 1 = 4。

考虑 [-4,-3,-2,-1] 的情况,它们应该落在一个桶中。

如果直接复用 idx = nums[i] / size 的话,[-4][-3,-2,-1] 会被分到不同的桶中。

根本原因是我们处理整数的时候,已经分掉了数值 0。

这时候我们需要先对 nums[i] 进行 +1 操作(即将负数部分在数轴上进行整体右移),即得到 (nums[i] + 1) / size。

这样一来负数部分与正数部分一样,可以被正常分割了。

但由于 0 号桶已经被使用了,我们还需要在此基础上进行 -1,相当于将负数部分的桶下标(idx)往左移,即得到 ((nums[i] + 1) / size) - 1

Leecode 340 至多包含 K 个不同字符的最长子串 Leecode 1004 最大连续1的个数 III Leecode 1208 尽可能使字符串相等 Leecode 1493 删掉一个元素以后全为 1 的最长子数组 Leecode 3 无重复字符的最长子串 [30. 串联所有单词的子串] Leecode 76 最小覆盖子串 [159. 至多包含两个不同字符的最长子串] Leecode 209 长度最小的子数组 [239. 滑动窗口最大值] Leecode 438 找到字符串中所有字母异位词 Leeocde 567 字符串的排列 [632. 最小区间] [727. 最小窗口子序列]
30. 串联所有单词的子串

  1. 最小覆盖子串

  2. 至多包含两个不同字符的最长子串

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
思路:
暴力法就是两个for,On2的time
滑动窗口就是要想好怎么滑,什么情况下滑,怎么创建这个滑动窗口,On
其实滑动窗口核心还是双指针,左右边界不就是两个指针决定的嘛

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size();
        int l = 0,res = 1e7,sum=0;
        for(int r = 0;r<n;++r){
            sum+=nums[r];//第三题也是这样,先由右指针去搞出窗口来
            while(sum>=target){//这个就是判断左边能不能进来
                res = min(res,r-l+1);
                sum-=nums[l++];//这题的核心在这里,也就是怎么滑
            }
        }
        return res==1e7?0:res;//要注意边界
    }
};
  1. 滑动窗口最大值

567. 字符串的排列

给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。

输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).

思路:滑动窗口
由于是 s2 中判断是否包含 s1 的排列,而且 s1 和 s2 均为小数。

可以使用数组先对 s1 进行统计,之后使用滑动窗口进行扫描,每滑动一次检查窗口内的字符频率和 s1 是否相等 ~

以下代码,可以作为滑动窗口模板使用:

PS. 你会发现以下代码和 643. 子数组最大平均数 I 和 1423. 可获得的最大点数 代码很相似,因为是一套模板。

初始化将滑动窗口压满,取得第一个滑动窗口的目标值

继续滑动窗口,每往前滑动一次,需要删除一个和添加一个元素

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        vector<int> cnt1(26),cnt2(26);
        int n = s1.size(),m = s2.size();
        if(n>m) return false;
        for(int i = 0;i<n;i++){//搞两个窗口出来,小的那个不动
            ++cnt1[s1[i]-'a'];
            ++cnt2[s2[i]-'a'];
        }
        if(cnt1==cnt2) return true;
        for(int i = n;i<m;i++){//滑大的那个
            ++cnt2[s2[i]-'a'];
            --cnt2[s2[i-n]-'a'];
            if(cnt1==cnt2) return true;
        }
        return false;
    }
};

时间复杂度:O(n)O(n)
空间复杂度:O(n)O(n)
但其实这是某道困难题的简化版,本题根据「字符」滑动,而 30. 串联所有单词的子串 则是根据「单词」来。但基本思路都是一样的,强烈建议你来试试 ~

  1. 最小区间

  2. 最小窗口子序列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值