题目描述

✅ 1100. 长度为 K 的无重复字符子串

题意分析

统计长度恰好为 k、且内部字符互不重复的连续子串数量。不同起点对应不同子串位置,即使内容相同也要分别计数。题目只包含小写英文字母。

相邻的定长子串仅相差一个移出的左端字符和一个移入的右端字符,因此不用每次重新检查全部 k 个字符。维护窗口内的字符频次,再记录当前有多少种字符发生重复,就能在每次滑动后直接判断。

解法:滑动窗口维护有效区间

核心思路

[!blue]

cnt[c] 表示字母 c 在当前窗口中的次数,dup 表示频次至少为二的字母种类数,res 累计合法窗口数。dup == 0 当且仅当窗口没有重复字符。

扫描右端 i 时先加入 s[i]:只有频次从一变二,才出现一种新的重复字母,需要增加 dup;从二变三等变化仍是同一种重复,不再增加。若 i >= k,再移除旧左端 s[i-k];只有频次从二变一,才解除一种重复,需要减少 dup。

加入和移除完成后,计数对应的区间恰好是 [max(0, i-k+1), i]。当 i >= k-1 时它才达到长度 k,此时若 dup == 0,将 res 加一。每个定长子串都有唯一的右端,所以扫描恰好检查所有候选一次,不会遗漏或重复计数。

窗口本身可以暂时含有重复字符,移动边界只为维持固定长度;是否计入答案由 dup 决定。k 大于字符串长度时没有完整窗口,入口直接返回零。

解题步骤

  • 加入右端并按阈值更新 dup。
  • 长度超过 k 时移出旧左端。
  • 窗口满 k 位后判断并计数。

代码实现

class Solution {
    public int numKLenSubstrNoRepeats(String s, int k) {
        if (k > s.length()) {
            return 0;
        }

        int[] cnt = new int[26];
        int dup = 0;
        int res = 0;

        for (int i = 0; i < s.length(); i++) {
            int idx = s.charAt(i) - 'a';

            cnt[idx]++;

            // 只有从一份变两份,才新增一个重复种类。
            if (cnt[idx] == 2) {
                dup++;
            }

            if (i >= k) {
                int leftIdx = s.charAt(i - k) - 'a';

                cnt[leftIdx]--;

                // 只有从两份降一份,才解除这一种重复。
                if (cnt[leftIdx] == 1) {
                    dup--;
                }
            }

            // 窗口已满且更新完成后才计数。
            if (i >= k - 1 && dup == 0) {
                res++;
            }
        }

        return res;
    }
}
func numKLenSubstrNoRepeats(s string, k int) int {
    if k > len(s) {
        return 0
    }

    cnt := make([]int, 26)
    dup := 0
    res := 0

    for i := 0; i < len(s); i++ {
        idx := s[i] - 'a'
        cnt[idx]++
        // 只有从一份变两份,才新增一个重复种类。
        if cnt[idx] == 2 {
            dup++
        }

        if i >= k {
            leftIdx := s[i-k] - 'a'
            cnt[leftIdx]--
            // 只有从两份降一份,才解除这一种重复。
            if cnt[leftIdx] == 1 {
                dup--
            }
        }

        // 窗口已满且更新完成后才计数。
        if i >= k-1 && dup == 0 {
            res++
        }
    }

    return res
}

复杂度分析

  • 时间复杂度:$O(n)$。每个字符入窗一次、至多出窗一次,每次只更新两个频次和重复种类数。
  • 空间复杂度:$O(1)$,频次数组固定为 26 个位置。

关键点总结

[!green]

  • dup 数的是重复的字符种类,不是多余字符总个数。
  • 第一次计数在右端下标 k−1,第一次移除在 k。

易错点总结

[!yellow]

  • 加入第三个相同字母又增加 dup,会重复记录同一种。
  • 从三减到二就解除重复,会把仍含重复的窗口算入。
  • 入窗后、出窗前就判断,会使用临时 k+1 长度窗口。
  • k == 1 时每个单字符窗口都合法;k > 26 时任何完整窗口都会重复,当前判断自然得到零。

相似题目

题目 难度 关联与区别
3. 无重复字符的最长子串 中等 原题求任意长度的最长无重复子串,本题窗口长度固定,只统计合法窗口数量。
438. 找到字符串中所有字母异位词 中等 同样对定长窗口维护字符频次,本题要求全部次数不超过1,原题要求与模式频次一致。
补充题 211. 长度为 k 的无重复字符子串枚举 中等 都维护长度为 k 的滑动窗口和字符频次;补充题返回全部符合条件的窗口。
159. 至多包含两个不同字符的最长子串 中等 用滑动窗口维护字符频次和有效左边界;本题固定窗口长度后统计无重复窗口,该题允许最多两种字符。
340. 至多包含 K 个不同字符的最长子串 中等 用滑动窗口维护字符频次和有效左边界;本题固定窗口长度后统计无重复窗口,该题允许最多 k 种字符。
904. 水果成篮 中等 用滑动窗口维护字符频次和有效左边界;本题固定窗口长度后统计无重复窗口,该题把两种水果限制转为两类元素窗口。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/73899351
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!