【LeetCode 热题 100】(三)滑动窗口

1. 无重复字符的最长字串

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int length = s.length();
        // 1. 定义全局最大长度
        int ans = 0;
        // 2. 构建一个滑动窗口
        HashSet<Character> hashSet = new HashSet<>();
        int ret = -1;
        for (int i = 0; i < length; i++) {
            if(i!=0){
                hashSet.remove(s.charAt(i-1));
            }
            while ((ret+1) < length && !(hashSet.contains(s.charAt(ret+1)))){
                hashSet.add(s.charAt(ret + 1));
                ret = ret + 1;
            }
            ans = Math.max(ret-i+1, ans);

        }
        return ans;

    }
}

解题思路:滑动窗口法(双指针优化)

关键思想

使用滑动窗口技术维护一个不含重复字符的子串窗口,通过双指针动态调整窗口边界,高效求解最长无重复子串长度。

核心步骤
  1. 初始化组件

    • left指针(代码中的 i):标记窗口起始位置(初始0)
    • right指针(代码中的 ret):标记窗口结束位置(初始-1,表示空窗口)
    • HashSet:存储当前窗口内字符(用于O(1)时间重复判断)
    • maxLength(代码中的 ans):记录全局最大长度(初始0)
  2. 滑动窗口操作

    无重复
    有重复/越界
    开始
    遍历左指针 i
    i>0?
    移除 i-1 位置的字符
    尝试扩展右边界
    右边界可扩展?
    添加字符并右移
    计算当前窗口长度
    更新最大长度
  3. 具体操作解析

    • 左指针移动(每次循环左移1位):
      • i > 0 时,从集合移除 i-1 位置的字符
      • 相当于窗口左边界右移,缩小窗口
    • 右指针扩展
      • right+1 位置字符不在集合中未越界
        • 添加该字符到集合
        • 右指针 right 右移(扩大窗口)
      • 重复直到遇见重复字符或字符串末尾
    • 长度更新
      • 计算当前窗口长度:right - i + 1
      • 更新全局最大值:maxLength = max(当前长度, maxLength)
时间复杂度
  • O(n):左右指针各遍历字符串一次
    • 左指针 i 严格右移 n
    • 右指针 right 只增不减,最多移动 n
  • 每个字符最多被加入/移除集合1次
空间复杂度
  • O(min(n, 字符集大小))
    • 英文字符集:O(26)
    • 全字符集:O(128) 或 O(256)
算法优势
  1. 单次遍历:通过右指针不回退,避免重复检查
  2. 实时更新:每次窗口变动后立即更新最大值
  3. 边界处理:天然支持空串(length=0)、单字符串等边界情况
示例演算(s=“pwwkew”)
左指针i移除字符右指针扩展当前窗口长度最大值
0-p→w→停(w重复)“pw”22
1pw(重复)→停止“w”12
2ww→k→e→停(w重复)“wke”33
3wk(已在)→停止“ke”23
4ke→w→停止“ew”23
5ew(重复)→停止“w”13

最终返回最大值 3(对应子串 "wke"

该实现完美体现了滑动窗口思想:通过动态调整窗口边界,高效维护解空间,在O(n)时间内解决经典子串问题。

2. 找到字符串中所有的字母异位词

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int s_len = s.length();
        int p_len = p.length();
        LinkedList<Integer> list = new LinkedList<>();
        if(s_len < p_len){
            return list;
        }
        int[] sCount = new int[26];
        int[] pCount = new int[26];

        for (int i = 0; i < p_len; i++) {
            int s_index = s.charAt(i) - 'a';
            sCount[s_index] = sCount[s_index] + 1;
            int p_index = p.charAt(i) - 'a';
            pCount[p_index] = pCount[p_index] + 1;

        }
        if(Arrays.equals(sCount,pCount)){
            list.add(0);
        }

        for (int i = 0; i < s_len - p_len; i++) {
            int s_index_1 = s.charAt(i) - 'a';
            sCount[s_index_1] =  sCount[s_index_1] - 1;
            int s_index_2 = s.charAt(i + p_len) - 'a';
            sCount[s_index_2] = sCount[s_index_2] + 1;

            if(Arrays.equals(sCount,pCount)){
                list.add(i+1);
            }
        }
        return list;
    }
}

解题思路:固定长度滑动窗口(字母异位词问题)

问题分析

给定字符串 s 和模式串 p,需要在 s 中找到所有是 p 的字母异位词(anagram)的子串起始索引。字母异位词指字母相同但排列不同的字符串(如 “ab” 和 “ba”)。

核心算法:频率计数+滑动窗口
  1. 边界检查

    • 如果 s 长度小于 p 长度,直接返回空结果(不可能存在异位词)
  2. 频率数组初始化

    • 创建两个长度26的数组(对应26个小写字母):
      • pCount:存储 p 的字符频率
      • sCount:存储 s 的滑动窗口字符频率
    int[] sCount = new int[26];
    int[] pCount = new int[26];
    
  3. 初始化窗口(0位置)

    • 同时统计 psp_len 个字符的频率
    for (int i = 0; i < p_len; i++) {
        sCount[s.charAt(i) - 'a']++;  // 统计s前p_len个字符
        pCount[p.charAt(i) - 'a']++;  // 统计p的所有字符
    }
    
    • 检查初始窗口是否匹配:if (Arrays.equals(sCount, pCount)) list.add(0);
  4. 滑动窗口(核心逻辑)

    • 窗口从位置0开始向右滑动:
      for (int i = 0; i < s_len - p_len; i++) {
          // 1. 移除左边界的字符
          sCount[s.charAt(i) - 'a']--;
          
          // 2. 添加右边界的字符
          sCount[s.charAt(i + p_len) - 'a']++;
          
          // 3. 检查新窗口是否匹配
          if (Arrays.equals(sCount, pCount)) {
              list.add(i + 1);  // 记录起始位置
          }
      }
      
    • 窗口移动演示
      初始窗口:s = [a b a b], p = "ab"
      窗口0: "ab" -> sCount={a:1,b:1} -> 匹配 -> 记录0
      
      i=0时:
         移除s[0]='a':sCount={a:0,b:1}
         添加s[2]='a':sCount={a:1,b:1}
         新窗口"ba" -> 匹配 -> 记录1
         
      i=1时:
         移除s[1]='b':sCount={a:1,b:0}
         添加s[3]='b':sCount={a:1,b:1}
         新窗口"ab" -> 匹配 -> 记录2
      
关键特性
  1. 固定窗口大小

    • 始终保持窗口长度 = p.length()
    • 通过 i 控制窗口起始位置
  2. 高效频率更新

    • O(1) 时间移除左边界字符
    • O(1) 时间添加右边界字符
    • O(26) 时间比较频率数组(常数时间)
  3. 时间复杂度

    • O(n) :遍历字符串一次(n = s.length())
    • 比暴力解法(O(n²))更高效
示例演示(s=“abab”, p=“ab”)
窗口起始位置窗口内容sCount [a,b]是否匹配结果列表
0“ab”[1,1][0]
1“ba”[1,1][0,1]
2“ab”[1,1][0,1,2]

最终输出:[0,1,2](三个异位词:“ab”、“ba”、“ab”)

算法总结

  1. 适用场景:寻找固定长度的字母异位词
  2. 核心技巧
    • 频率数组代替哈希表(小写字母场景)
    • 滑动窗口避免重复计算
  3. 优势
    • 时间复杂度 O(n)
    • 空间复杂度 O(1)(固定26长度数组)
  4. 扩展性
    • 可处理包含大写/特殊字符的场景(扩大数组长度)
    • 可扩展为找最小覆盖子串等问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值