力扣hot100滑动窗口(C++)

一、无重复字符的最长子串

(s由英文字母、数字、符号和空格组成)

1.1题目解析

这道题要求找到一个字符串中不包含重复字符的最长子串的长度。子串必须是连续的字符序列,不是子序列,不能跳着选字符。

1.2初试

我最开始的想法很简单:把每个可能的子串都检查一遍。从字符串的第一个字符开始,看看能往右延伸多远不出现重复字符,记录这个长度,然后从第二个字符开始同样检查,一直试完所有起始位置。

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        if(n == 0) return 0;
        int maxlength = 1;
        
        for(int i = 0; i < n; i++) {
            int a[256] = {0};  // 用数组记录字符出现次数
            
            for(int j = i; j < n; j++) {
                int k = (int)s[j];  // 字符转成ASCII码值
                a[k]++;
                
                if(a[k] == 2) {  // 某个字符出现两次,说明有重复
                    maxlength = max(maxlength, j - i);
                    break;
                }
                maxlength = max(maxlength, j - i + 1);
            }
        }
        return maxlength;
    }
};

这个方法很直观,但效率太低。外层循环遍历每个起始位置,内层循环从起始位置向右延伸检查重复。对于长度为n的字符串,最坏情况下要检查n²/2个子串,时间复杂度是O(n²)。当字符串很长时,这个算法会超时。

1.3优化思路:滑动窗口

注意到暴力解法中有很多重复计算:比如检查从i到j的子串没有重复后,检查从i到j+1的子串时,其实只需要判断s[j+1]这个字符是否在前面出现过,不需要重新检查整个子串。

如果用滑动窗口的方法,维护一个窗口,窗口里的字符都是不重复的。右指针不断向右扩展窗口,当遇到重复字符时,左指针向右移动直到窗口内没有重复字符。

cpp 复制代码
int lengthOfLongestSubstring(string s) {
    unordered_map<char, int> charIndex;  // 记录每个字符最近出现的位置
    int n = s.size();
    int maxlength = 0;
    int left = 0;  // 窗口左边界
    
    for(int right = 0; right < n; right++) {
        // 如果当前字符已存在且在窗口内
        if(charIndex.count(s[right]) && charIndex[s[right]] >= left) {
            left = charIndex[s[right]] + 1;  // 左边界跳到重复字符的下一位
        }
        charIndex[s[right]] = right;  // 更新字符位置
        maxlength = max(maxlength, right - left + 1);  // 更新最大长度
    }
    return maxlength;
}

这个算法的关键是用哈希表记录每个字符最近出现的位置。右指针一直向右移动,左指针只有遇到重复字符时才移动。这样每个字符最多被访问两次(右指针一次,左指针一次),时间复杂度降到O(n)。

这里有个细节要注意:判断字符是否在窗口内时,不仅要看这个字符是否出现过,还要看它最近出现的位置是否在left的右边。因为字符可能在窗口外面出现过,这种重复不影响当前窗口。


二、 找到字符串中所有字母异位词

(s和p只包含小写字母)

2.1题目解析

这道题要在字符串s中找到所有是p的异位词的子串,返回这些子串的起始索引。异位词指的是字母相同但顺序可以不同的词,比如"abc"和"bca"是异位词。

2.2初试

看到这道题,我觉得很适合用滑动窗口,因为要找的是固定长度的子串。窗口长度就是p的长度,我们需要检查每个这个长度的窗口是否是p的异位词。

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        if(s.length() < p.length()) return result;  // s比p短,不可能有异位词
        
        // 记录p中每个字符的出现次数
        vector<int> pCount(26, 0);
        for(char c : p) {
            pCount[c - 'a']++;
        }
        
        // 记录窗口内字符的出现次数
        vector<int> windowCount(26, 0);
        int windowSize = p.length();
        
        // 初始化第一个窗口
        for(int i = 0; i < windowSize; i++) {
            windowCount[s[i] - 'a']++;
        }
        
        // 检查第一个窗口
        if(windowCount == pCount) {
            result.push_back(0);
        }
        
        // 滑动窗口
        for(int i = windowSize; i < s.length(); i++) {
            // 移除窗口左边字符
            windowCount[s[i - windowSize] - 'a']--;
            // 添加窗口右边新字符
            windowCount[s[i] - 'a']++;
            
            // 检查当前窗口是否是异位词
            if(windowCount == pCount) {
                result.push_back(i - windowSize + 1);
            }
        }
        
        return result;
    }
};

这个方法维护一个固定长度的窗口,每次滑动时更新窗口内字符计数,然后比较窗口计数和p的计数是否相同。时间复杂度是O(n),空间复杂度是O(1)(因为只有26个小写字母)。

2.3进一步优化

上面的方法需要每次都比较两个26位的数组,虽然常数时间,但还可以优化。可以用一个变量记录窗口和p的差异程度。

cpp 复制代码
vector<int> findAnagrams(string s, string p) {
    vector<int> result;
    int sLen = s.length(), pLen = p.length();
    if(sLen < pLen) return result;
    
    vector<int> count(26, 0);
    // 初始化:p中的字符加1,窗口中的字符减1
    // 这样如果count中某个字符为0,表示这个字符在p和窗口中数量相同
    for(int i = 0; i < pLen; i++) {
        count[p[i] - 'a']++;
        count[s[i] - 'a']--;
    }
    
    int diff = 0;  // 记录有多少个字符的数量不匹配
    for(int num : count) {
        if(num != 0) diff++;
    }
    
    if(diff == 0) result.push_back(0);
    
    // 滑动窗口
    for(int i = pLen; i < sLen; i++) {
        char leftChar = s[i - pLen] - 'a';
        char rightChar = s[i] - 'a';
        
        // 移除左边字符
        if(count[leftChar] == 0) diff++;  // 原来匹配,现在移除后不匹配了
        count[leftChar]++;
        if(count[leftChar] == 0) diff--;  // 现在又匹配了
        
        // 添加右边字符
        if(count[rightChar] == 0) diff++;  // 原来匹配,现在添加后不匹配了
        count[rightChar]--;
        if(count[rightChar] == 0) diff--;  // 现在又匹配了
        
        if(diff == 0) result.push_back(i - pLen + 1);
    }
    
    return result;
}

这个优化的好处是不用每次比较两个数组,只需要维护一个diff变量,表示有多少个字符在窗口和p中数量不同。当diff为0时,窗口就是p的异位词。每次窗口滑动时,只需要更新移出字符和移入字符对diff的影响。

三、算法特点总结

滑动窗口算法特别适合处理连续子串或子数组问题。它的核心思想是维护一个窗口,通过移动窗口的左右边界来遍历所有可能的子串/子数组,同时高效地更新窗口状态。

3.1窗口的三种类型:

  • 固定长度窗口:像第二题,窗口长度固定为p的长度,每次滑动一个位置检查

  • 可变长度窗口:像第一题,窗口长度根据条件变化,右指针扩展窗口,左指针在条件不满足时收缩窗口

  • 多指针窗口:有些复杂问题可能需要多个指针协同工作

3.2滑动窗口的优点:

  • 通常能将O(n²)的暴力解法优化到O(n)

  • 思路清晰,代码结构统一

  • 适合处理连续区间问题

写在最后

滑动窗口是一个很实用的技巧,掌握后能解决很多字符串和数组问题。关键是多练习,理解不同问题中窗口的含义和移动规则。

如果还有其他想法的小伙伴可以在评论区积极讨论。

相关推荐
皮皮哎哟19 小时前
数据结构:嵌入式常用排序与查找算法精讲
数据结构·算法·排序算法·二分查找·快速排序
程序员清洒19 小时前
CANN模型剪枝:从敏感度感知到硬件稀疏加速的全链路压缩实战
算法·机器学习·剪枝
敏叔V58719 小时前
AI智能体的工具学习进阶:零样本API理解与调用
人工智能·学习
vortex519 小时前
几种 dump hash 方式对比分析
算法·哈希算法
2501_9418649620 小时前
科学方法论破解学习时间堆砌误区
学习
堕27420 小时前
java数据结构当中的《排序》(一 )
java·数据结构·排序算法
初願致夕霞20 小时前
Linux_进程
linux·c++
2302_8138062220 小时前
【嵌入式修炼:数据结构篇】——数据结构总结
数据结构
Thera77720 小时前
【Linux C++】彻底解决僵尸进程:waitpid(WNOHANG) 与 SA_NOCLDWAIT
linux·服务器·c++
Wei&Yan20 小时前
数据结构——顺序表(静/动态代码实现)
数据结构·c++·算法·visual studio code