当前位置: 首页 > article >正文

滑动窗口核心算法解决字符串问题(最小覆盖子串/字符串排列/异位词/最长无重复子串)

滑动窗口核心算法解决字符串问题

滑动窗口概览

滑动窗口技巧主要解决子数组问题,比如寻找符合某个条件最长/最短的子数组。

思想:维护一个窗口,不断滑动,更新答案
int left = 0, right = 0;

while (right < nums.size()) {
    // 增大窗口
    window.addLast(nums[right]);
    right++;
    
    while (window needs shrink) {
        // 缩小窗口
        window.removeFirst(nums[left]);
        left++;
    }
}
  • 基于滑动窗口算法框架写出的代码,时间复杂度是 O(N),比嵌套 for 循环的暴力解法效率高。

  • 指针 left, right 不会回退(它们的值只增不减),所以字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比。

伪代码框架:
void slidingwindow(string s){
    //合适数据结构记录窗口数据 map/int
    auto window= ;
    int left=0,right=0;
    //窗口扩大(必做)
    while(right<s.size()){
        //c是将移入窗口的字符
        char c=s[right];
        window.add(c);
        //增大窗口
        right++;
        //进行一系列操作
        ...
               
        
        //判断左侧是否要收缩(选做)
        while(window needs shrink){
            //d是一处窗口的字符
            char d=s[left];
            window.remove(d);
            //缩小窗口
            left++;
            //进行窗口内数据一系列操作
            ...

                
        }
    }
}

这两个 ... 处的操作分别是扩大和缩小窗口的更新操作,其实它们操作是完全对称的。

LeeCode案例

一、最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

思路
  1. 双指针–>左右指针—>[left, right) ---->一个「窗口」(方便窗口内只有一个元素)
  2. 增大窗口,直到[left, right) 包含t
  3. 停止增大,开始缩小窗口,直到不符合要求,不 包含t
  4. 重复2,3

第 2 步相当于在寻找一个「可行解」,然后第 3、4 步在优化这个「可行解」,最终找到最优解

图解:

增大窗口,直到包含t:

img

缩小窗口:

img

不再符合条件:

img

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

完整代码:
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) {
            need[c]++;
        }

        int left = 0, right = 0;
        // 记录window中的字符满足need条件的字符个数
        int valid = 0;
        // 记录最小覆盖子串的起始索引及长度
        int start = 0, len = INT_MAX;
        while (right < s.size()) {
            // c 是将移入窗口的字符
            char c = s[right];
            // 扩大窗口
            right++;
            // 进行窗口内数据的一系列更新
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }

            // 判断左侧窗口是否要收缩
            while (valid == need.size()) {

                // 在这里更新最小覆盖子串
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                // d 是将移出窗口的字符
                char d = s[left];
                // 缩小窗口
                left++;
                // 进行窗口内数据的一系列更新
                if (need.count(d)) {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;

                }
            }

        }
        // 返回最小覆盖子串
        return len == INT_MAX ? "" : s.substr(start, len);
    }
};
二、字符串排列

给你两个字符串 s1s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false

换句话说,s1 的排列之一是 s2子串

注意哦,输入的 s1 是可以包含重复字符的,所以这个题难度不小。

思路:

不再赘述,主要是看更新窗口需要有什么变化

主要思考以下问题:

  1. 什么时候扩大窗口,窗口加入字符时,应该更新那些数据?
  2. 窗口什么时候停止扩大,此时又该更新哪些数据?
  3. 我们要的结果应该在扩大还是缩小窗口时更新?
完整代码:(cp后改)-------错
class Solution {
public:
    // 判断 s 中是否存在 t 的排列
    bool checkInclusion(string t, string s) {
        unordered_map<char, int> need, window;
        for (char c : t) {
            need[c]++;
        }

        int left = 0, right = 0;
        // 记录window中的字符满足need条件的字符个数
        int valid = 0;
        // 不需记录最小覆盖子串的起始索引及长度
        //int start = 0, len = INT_MAX;
        while (right < s.size()) {
            // c 是将移入窗口的字符
            char c = s[right];
            // 扩大窗口
            right++;
            // 进行窗口内数据的一系列更新
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }

            // 判断左侧窗口是否要收缩
            while (right-left>=t.size()) {
                
                //判断是否找到了合法的子串
                if(valid==need.size()) return true;
                
                // d 是将移出窗口的字符
                char d = s[left];
                // 缩小窗口
                left++;
                // 进行窗口内数据的一系列更新
                if (need.count(d)) {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;

                }
            }

        }
        return false;
    }
};
问题解决:

在这里插入图片描述

错误原因: if(valid=need.size()) 中用了赋值运算符 =,而不是比较运算符 ==,因此这导致了错误的逻辑。

在这里插入图片描述

三、找所有字母异位词

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

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

思路:

这个所谓的字母异位词,不就是排列吗,搞个高端的说法就能糊弄人了吗?相当于,输入一个串 S,一个串 T,找到 S 中所有 T 的排列,返回它们的起始索引

代码:
class Solution {
public:
    vector<int> findAnagrams(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) {
            need[c]++;
        }

        int left = 0, right = 0;
        int valid = 0;
        // 记录结果
        vector<int> res;
        while (right < s.size()) {
            char c = s[right];
            right++;
            // 进行窗口内数据的一系列更新
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) {
                    valid++;
                }
            }
            // 判断左侧窗口是否要收缩
            while (right - left >= t.size()) {
                // 当窗口符合条件时,把起始索引加入 res
                if (valid == need.size())
                    res.push_back(left);
                char d = s[left];
                left++;
                // 进行窗口内数据的一系列更新
                if (need.count(d)) {
                    if (window[d] == need[d]) {
                        valid--;
                    }
                    window[d]--;
                }
            }
        }
        return res;
    }
};

与找字符串排列一样,只是找到一个合法异位词(排列)之后将其索引加入 res 即可。

四、最长无重复子串

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

思路:

找到满足条件的子串,更新res为最大子串即可

代码:
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left=0,right=0;
        unordered_map<char,int> window;
        //记录结果
        int res=0;
        while(right<s.size()){
            char c=s[right];
            right++;

            window[c]++;

            //判断是否收缩
            while(window[c]>1){
                char d=s[left];
                left++;
                window[d]--;
            }
            res=max(res,right-left);
        }
        return res;        
    }
};
总结:

模版即为:

在这里插入图片描述

其中判断左侧是否要收缩时:

判断条件有:

  1. window[c] > 1:最右侧字符有重复就收缩
  2. right - left >= t.size():窗口不满足子串t的大小就收缩
  3. valid == need.size():已经满足字符个数的个数满了就收缩,使得到最小子串。

http://www.kler.cn/a/539034.html

相关文章:

  • 智能化食品安全管理:AI视频监控在大型商场的技术方案
  • 排序合集(一)
  • 【异常解决】在idea中提示 hutool 提示 HttpResponse used withoud try-with-resources statement
  • 【C语言标准库函数】指数与对数函数:exp(), log(), log10()
  • JVM虚拟机以及跨平台原理
  • 2020-12-27 把int类型拆开并放入一个字符型数组当中。
  • [vue3] Ref Reactive
  • 如何在Python中使用内置函数
  • 【Golang学习之旅】Go + Redis 缓存设计与优化(项目实战)
  • 2.9学习总结
  • 从零开始了解人工智能:核心概念、GPT及 DeepSeek 探索
  • 使用cursor开发python调用deepseek本地模型实现本地AI对话
  • 如何学习多智能体系统协调(如自动驾驶车协同避让)
  • Linux:安装 node 及 nvm node 版本管理工具(ubuntu )
  • jvm view
  • 【LeetCode Hot100 堆】第 K 大的元素、前 K 个高频元素
  • 智慧城市节水管理信息系统项目解决方案
  • 在阿里云ECS上一键部署DeepSeek-R1
  • 7.Python文件操作:文件的打开与关闭、文件的读写、文件读写应用
  • 数据管理的“圣经”——《DAMA数据管理知识体系指南(第二版)》
  • 解锁 DeepSeek 模型高效部署密码:蓝耘平台深度剖析与实战应用
  • React 什么是抽象组件及为什么要抽象组件
  • 人工智能-A* 算法规划的路径进行动态调整
  • 分组加密算法CLEFIA
  • 【LLM】o1/R1系列LLM数据篇
  • 【开学补课复习专题】python 语言考试试题2