力扣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)

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

  • 适合处理连续区间问题

写在最后

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

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

相关推荐
求真求知的糖葫芦1 小时前
巴伦学习(一)一种新型补偿传输线巴伦论文学习笔记(自用)
笔记·学习·射频工程
钱彬 (Qian Bin)1 小时前
项目实践19—全球证件智能识别系统(优化检索算法:从MobileNet转EfficientNet)
算法·全球证件识别
feifeigo1232 小时前
基于EM算法的混合Copula MATLAB实现
开发语言·算法·matlab
freepopo2 小时前
书房设计|3㎡书桌角,治愈学习时光 [特殊字符]
学习
鑫—萍2 小时前
嵌入式开发学习——STM32单片机入门教程
c语言·驱动开发·stm32·单片机·嵌入式硬件·学习·硬件工程
漫随流水2 小时前
leetcode回溯算法(78.子集)
数据结构·算法·leetcode·回溯算法
斐夷所非2 小时前
C++ 继承、多态与类型转换 | 函数重载 / 隐藏 / 覆盖实现与基派生类指针转换
c++
S火星人S2 小时前
LVGL[display]
单片机·学习
全栈游侠2 小时前
数据结构 - 链表
数据结构·链表