子数组计数、双指针与前缀和

共 32 题
📑 题目列表 32 题
#
★★★

1. 三数之和(15)如何排序+双指针+去重避免超时

三数之和(LeetCode 15)如何用排序 + 双指针 + 去重来避免超时?

  • 排序 + 双指针
  • 去重策略
  • 去重策略:固定一个数后,对左右指针值去重,避免重复三元组

先对数组排序,然后固定第一个数 nums[i],用左右双指针在 i 右侧找两个数使其和为 -nums[i]。因为排序后双指针可单调收缩:和小于目标则左指针右移,大于则右指针左移。同时用"跳过重复值"去重:固定 i 时若 nums[i]==nums[i-1] 跳过;找到一组解后,左右指针跳过重复值再继续。这样复杂度 O(n²)(排序 O(n log n) + 双指针 O(n²)),比暴力三重循环 O(n³) 快得多,是标准解法。

排序使双指针可行(单调性依据),也便于去重。双指针在有序数组上找两数和可用 O(n),固定 i 后对每个 i 做一次 O(n) 双指针,总 O(n²)。去重通过"跳过与上一个相同的元素"避免重复三元组,避免输出重复结果。

List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    for (int i = 0; i < nums.length - 2; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重
        int l = i + 1, r = nums.length - 1, target = -nums[i];
        while (l < r) {
            int s = nums[l] + nums[r];
            if (s == target) {
                res.add(Arrays.asList(nums[i], nums[l], nums[r]));
                while (l < r && nums[l] == nums[l + 1]) l++; // 去重
                while (l < r && nums[r] == nums[r - 1]) r--;
                l++; r--;
            } else if (s < target) l++;
            else r--;
        }
    }
    return res;
}
#
★★★

2. 为什么三数之和要先排序而非用哈希(去重与复杂度权衡)

为什么三数之和要先排序而非直接用哈希(去重与复杂度的权衡)?

  • 排序 vs 哈希
  • 去重的便利性
  • 复杂度权衡:排序 O(n log n) 换取去重与双指针的线性扫描

虽然哈希可在 O(n²) 内找两数和(两数之和),但三数之和的关键难点是"去重"——避免重复三元组。哈希法虽然能找组合,但去重很麻烦(需要记录已用过的组合,或对结果排序去重),且在找三元组时容易重复。排序 + 双指针天然有序,去重只需"跳过相同值",简单高效;同时排序后双指针还能做剪枝(和过小/过大提前结束)。两者复杂度同为 O(n²),但排序法去重更清晰,故优先选择。

权衡点在于"去重成本"。哈希法省去排序但去重困难;排序法 (O(n log n)) 排序成本可接受,换来双指针的简单去重和剪枝。对"要输出所有不重复组合"的问题,排序 + 双指针更优。若只需判断是否存在,哈希也可行。

#
★★★

3. 前缀和与差分的互逆关系及恢复原数组的扫描方式

前缀和与差分是什么关系?如何通过差分恢复原数组?

  • 前缀和与差分的互逆
  • 差分数组还原
  • 差分还原:对差分数组做前缀和即可恢复原数组,与构建互逆

前缀和与差分互为逆运算:对数组 a,前缀和数组 p[i] = p[i-1] + a[i];对数组 a,差分数组 d[i] = a[i] - a[i-1](d[0]=a[0])。它们是互逆的——对 a 求差分再求前缀和,或求前缀和再求差分,都能还原 a。恢复原数组时,从差分数组做一次前缀和扫描即可:a[i] = a[i-1] + d[i](即前缀和累加)。

前缀和解决"区间求和",差分解决"区间更新"。两者互为逆:差分数组的"前缀和"就是原数组,前缀和数组的"差分"就是原数组。利用差分做区间加后,只需一次前缀和扫描即可恢复原数组,这是差分的基础应用。

#
★★★

4. 前缀和配合哈希表统计"和为 K 的子数组个数"(560)

前缀和配合哈希表如何统计"和为 K 的子数组个数"(LeetCode 560)?

  • 前缀和 + 哈希
  • 子数组计数
  • 哈希计数:用 map 记录前缀和出现次数,O(1) 查询和为 K 的子数组个数

子数组 [j..i] 的和 = prefix[i] - prefix[j-1]。要判断是否等于 K,对于每个位置 i,需要统计"前面有多少个前缀和等于 prefix[i] - K"。用哈希表记录每个前缀和出现的次数,遍历时对每个 i 累加 map[prefix[i] - K],然后更新 map[prefix[i]]。初始时 map[0]=1(表示空前缀和为 0)。这样 O(n) 时间统计所有满足条件的子数组个数。

关键在于把"子数组和"转化为"两个前缀和之差",再用哈希表 O(1) 统计"等于某值的前缀和个数"。这样避免枚举所有子数组 O(n²)。注意 map[0]=1 处理从 0 开始的子数组。

int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);
    int sum = 0, ans = 0;
    for (int x : nums) {
        sum += x;
        ans += map.getOrDefault(sum - k, 0);
        map.put(sum, map.getOrDefault(sum, 0) + 1);
    }
    return ans;
}
#
★★★

5. 有序数组合并/区间交集如何用双指针线性扫描

有序数组合并、区间交集如何用双指针线性扫描?

  • 双指针线性扫描
  • 有序序列合并/交集
  • 线性扫描:双指针在有序数组上按序推进,合并或求交集均 O(n)

有序数组用双指针分别指向两个数组,比较当前元素,按需要合并(取较小者进结果)或求交集(相等时记录并两指针都前移)。区间交集用双指针分别指向两个区间列表,比较区间端点,若相交则记录交集,并移动"右端点较小"的那个区间的指针。因为有序,指针只前进,每个元素访问一次,整体 O(m+n)。

有序保证双指针可行:合并时取较小者,交集时移动"结束更早"的区间。指针单调前进,无重复扫描,复杂度 O(m+n)。这是有序结构下双指针的经典应用。

#
★★★

6. 滑动窗口中位数/第 K 大(结合平衡树或双堆)

滑动窗口中位数/第 K 大如何实现(结合平衡树或双堆)?

  • 双堆维护中位数
  • 平衡树/延迟删除
  • 双堆维护:大顶堆+小顶堆平衡中位数,配合延迟删除处理滑动窗口

滑动窗口的中位数需要维护"窗口内有序集合",常用双堆(一个大顶堆 + 一个小顶堆,中间维持两个堆大小差 ≤ 1)或平衡树。双堆中,大顶堆存较小的一半、小顶堆存较大的一半,中位数是堆顶之一。但窗口滑动要删除出窗元素,堆不支持任意删除,需用"延迟删除"(记录待删除元素计数,出窗时标记,堆顶是待删元素时懒删除)。平衡树(有序集合)则支持 O(log k) 的插入删除和 O(1) 取中位数。滑动 n 个元素,总复杂度 O(n log k)。

中位数需要"完整有序信息",单调队列无法胜任,只能靠有序结构。双堆 + 延迟删除是空间 O(k) 的常用方案;平衡树更直接但可能更慢。第 K 大同理可用小顶堆(固定大小 k)或平衡树。复杂度均为 O(n log k)。

#
★★★

7. 用前缀和 O(1) 求任意子数组和及如何避免越界处理

如何用前缀和 O(1) 求任意子数组和,并避免越界处理?

  • 前缀和 O(1) 查询
  • 下标越界处理
  • 越界处理:前缀和数组下标从 1 开始,用 pre[r]-pre[l-1] 避免边界讨论

前缀和数组 p[0..n],p[i] 表示前 i 个元素之和(p[0]=0)。子数组 [l, r](下标从 0 起)的和 = p[r+1] - p[l]。用 p[0]=0 作为哨兵,使 l=0 时 p[l] 有定义,避免越界。这样任意子数组和都能 O(1) 求出,且无需对 l=0 做特判。

关键是把前缀和数组定义为长度 n+1,p[0]=0 表示空前缀。这样"前 i 个"用 p[i] 表示,子数组和恒为 p[r+1]-p[l],下标 l 从 0 开始也不越界。这是前缀和"偏移 +1"规避边界处理的经典技巧。

int sumRange(int[] nums, int l, int r) { // 闭区间 [l,r]
    int[] p = new int[nums.length + 1];
    for (int i = 0; i < nums.length; i++) p[i + 1] = p[i] + nums[i];
    return p[r + 1] - p[l];
}
#
★★★

8. 最小覆盖子串(76)如何用 need/found 双计数精确收缩

最小覆盖子串(LeetCode 76)如何用 need/found 双计数精确收缩?

  • 双哈希计数
  • 精确收缩
  • need/found 计数:need 记录需求量、found 记录已有量,精确判断是否满足

用 need 哈希记录目标串 t 中每个字符的需求次数,用 found 记录当前窗口已覆盖的计数,以及一个变量 matched 记录"已满足需求的字符种类数"。右指针扩展加入字符,当某字符计数达到 need 时 matched++;当 matched == need 的字符种类数时窗口合法,此时尝试左指针收缩:移除字符时若该字符计数跌到 need 之下则 matched--,直到不能再缩。收缩后记录最小窗口。这样精确控制"必需字符"的匹配,得到最小覆盖子串。

关键在于"matched 只在该字符计数达到需求时 +1、跌破需求时 -1",从而精确反映窗口是否覆盖 t 的所有字符。收缩时先判断"该字符是否仍必需"(即移除后是否仍满足需求),只有影响了必需性才更新 matched。这比直接比较两个哈希表更快更精确。

#
★★★

9. 统计"无重复字符的最长子串"(3)如何用 last 哈希 O(n)

统计"无重复字符的最长子串"(LeetCode 3)如何用 last 哈希 O(n) 求解?

  • last 哈希记录最近位置
  • 无重复窗口
  • last 哈希:记录每个字符最近出现位置,遇到重复时左边界前移

维护左指针 l 和一个哈希表 last(记录每个字符最近一次出现的下标)。右指针 r 右扩时,若当前字符 s[r] 在 last 中且 last[s[r]] >= l,说明窗口内已有重复,需要把 l 移动到 last[s[r]] + 1(跳过重复字符)。然后更新 last[s[r]] = r,并记录窗口长度 r-l+1。每个字符进出一次,整体 O(n)。

关键是用 last 记录"每个字符最近位置",当遇到重复时直接跳到重复位置之后,而不是逐个收缩左指针。这样 l 单调前移,每个字符处理一次,O(n)。last 使"跳过重复"一步到位。

int lengthOfLongestSubstring(String s) {
    int[] last = new int[128];
    Arrays.fill(last, -1);
    int l = 0, ans = 0;
    for (int r = 0; r < s.length(); r++) {
        char c = s.charAt(r);
        if (last[c] >= l) l = last[c] + 1; // 跳到重复位置之后
        last[c] = r;
        ans = Math.max(ans, r - l + 1);
    }
    return ans;
}
#
★★

10. 为什么统计子数组和为 K 时要用"前缀和出现次数"而非枚举

为什么统计子数组和为 K 时要用"前缀和出现次数"而非枚举所有子数组?

  • 前缀和 + 哈希的计数思想
  • 复杂度对比
  • 计数思想:把"枚举子数组"转化为"统计前缀和出现次数",O(n) 求解

枚举所有子数组需要 O(n²) 时间(每个起点终点组合),无法应对大数据。用"前缀和出现次数"则把每个子数组 [l..r] 的和转化为 prefix[r] - prefix[l-1],只需在遍历时用哈希表统计 prefix[r] - K 出现的次数,单次 O(1),整体 O(n)。这是因为"子数组和 = 前缀和之差"这一性质把"组合计数"转化为"哈希查找",大幅降复杂度。

枚举 O(n²) 是因为要逐个检查所有子数组;前缀和 + 哈希利用"每个子数组对应一对前缀和",把"有多少个前缀和等于某值"用哈希 O(1) 统计,从而降为 O(n)。这是"空间换时间"的典型,也是子数组和类问题的通用范式。

#
★★

11. 前缀和取模(同余)在"可被 K 整除子数组"中的运用

前缀和取模(同余)在"可被 K 整除的子数组"问题中如何运用?

  • 前缀和取模
  • 同余计数
  • 同余计数:前缀和取模后,相同余数的两前缀之间即为可整除子数组

子数组 [l..r] 的和能被 K 整除,等价于 prefix[r] ≡ prefix[l-1] (mod K)(两前缀和同余)。因为 prefix[r] - prefix[l-1] 能被 K 整除当且仅当两者对 K 取模相等。因此用哈希(或数组)统计每个"前缀和 mod K"出现次数,对每个 r,累加"相同余数在此之前出现次数"即为可被 K 整除的子数组数。初始要处理余数 0(空前缀)。注意负数取模需调整为正余数。

同余思想把"可被 K 整除"转化为"前缀和余数相等",用桶计数 O(n) 求解。这是"可被 K 整除子数组"(LeetCode 974)的核心。处理负余数时用 (sum % K + K) % K 归一化。

#
★★

12. 前缀和数组下标偏移(+1)技巧对处理空前缀的帮助

前缀和数组下标偏移(+1)技巧对处理空前缀有什么帮助?

  • 下标偏移
  • 空前缀处理
  • 空前缀处理:下标 +1 使 pre[0]=0 代表空前缀,统一边界公式

定义前缀和数组 p[0..n],p[0]=0 表示"空前缀"(前 0 个元素和)。下标偏移 +1 后,p[i] 表示前 i 个元素之和,子数组 [l..r] 的和 = p[r+1] - p[l]。这样当 l=0 时 p[l]=p[0]=0 有定义,无需特判"从数组开头开始"的子数组。在"和为 K 的子数组"等计数题中,初始化 map[0]=1 正是利用空前缀参与计数,避免漏掉从 0 开始的子数组。

偏移 +1 的核心价值是"空前缀有明确表示 (p[0]=0)",从而统一处理边界。无论是求和查询还是计数,空前缀都能正常参与,避免 if(l==0) 特判。这是前缀和的标准约定。

#
★★

13. 平方有序数组(977)双指针从两端取极值归并

平方有序数组(LeetCode 977)如何用双指针从两端取极值归并?

  • 双指针从两端
  • 平方后归并
  • 两端取极值:平方后最大值在两端,用双指针从两端向中间归并

原数组有序,但包含负数时平方后不一定有序。观察到平方后最大值一定来自数组两端(绝对值最大的元素),因此用双指针 l、r 指向两端,每次比较 nums[l]² 和 nums[r]²,取较大者填入结果数组的末尾,并移动对应指针。这样从两端向中间收敛,从大到小填结果,得到平方后的有序数组。复杂度 O(n)。

关键观察到"平方后最大值在两端,最小值在中间"。双指针从两端取绝对值较大者,从结果末尾往前填,天然得到升序。因为原数组有序,负数的绝对值从左到右递减、正数绝对值从左到右递增,两端收敛取最大即可归并。

#
★★

14. 盛最多水的容器(11)双指针按"短板决定水量"的贪心推进

盛最多水的容器(LeetCode 11)双指针如何按"短板决定水量"贪心推进?

  • 短板决定水量
  • 双指针贪心
  • 贪心推进:缩小短板一侧,只有移动短板指针才可能增大水量

用左右指针指向数组两端,容器水量 = min(左高, 右高) × 宽度。每次移动"较矮"那一侧的指针(向内),因为移动较高一侧,min 不变、宽度减小,水量必然不增;而移动较矮一侧,虽然宽度减小,但可能遇到更高的墙抬高 min,从而有机会增大水量。因此每次移动较矮的一侧,记录所有水量取最大,即可得到最大容量。复杂度 O(n)。

贪心依据是"短板决定水量":当前水量受限于较矮一侧,移动较矮一侧才有机会让瓶颈变高;移动较高一侧因 min 不变而宽度减小,不可能更优。故每次移动较矮侧,保证不遗漏最优解,O(n) 完成。

#
★★

15. 有序数组两数之和(167)双指针收缩的正确性与单调性依据

有序数组两数之和(LeetCode 167)双指针收缩的正确性与单调性依据是什么?

  • 双指针单调性
  • 有序性依据
  • 单调性依据:有序性保证指针移动方向唯一且正确,不遗漏解

对有序数组,用左右指针指向首尾。若 nums[l] + nums[r] > target,为了减小和,只能右指针左移(因为左指针右移会增大和);若 < target,则左指针右移。由于数组有序,指针移动方向和值的变化方向一致,形成单调收缩,不会跳过正确解。每次移动一个指针,最坏 O(n) 找到目标或不存在。

单调性依据是"有序数组中,左指针右移只增不减、右指针左移只减不增"。当和偏大时,只有右移左指针会更大,所以必须左移右指针;反之亦然。这个"指针移动方向由和与目标的大小关系唯一决定"的单调性,保证了双指针不会遗漏正确解。

#
★★

16. 滑动窗口与位掩码结合处理"子数组 OR 值≥目标"类题

滑动窗口与位掩码结合如何处理"子数组 OR 值 ≥ 目标"类问题?

  • 位掩码维护
  • 滑动窗口与 OR
  • 位运算维护:OR 值单调不减,用位掩码合并窗口内元素的 OR 结果

这类题常用"值域有限的位计数"技巧:对每个二进制位维护窗口内该位"为 1 的个数",窗口的 OR 值可通过逐位判断"该位计数>0"得到。当需要判断"窗口 OR ≥ target"时,比较窗口 OR 与 target 的每一位。滑动窗口右扩左缩时,只需增减对应位的计数,O(1) 更新 OR 值。由于 OR 单调不减,可用贪心/窗口框架。复杂度 O(n × 位数)。

位掩码技巧源于"OR 的每位独立":某位为 1 当且仅当窗口内至少一个元素该位为 1。用位计数数组维护,窗口移动时 O(位数) 更新。OR 值随窗口扩大单调不减,这一单调性让滑动窗口可行。注意 OR 值无法用"减法"退化,所以用位计数而非直接存 OR 值。

#
★★

17. 一维差分如何实现"区间 [l,r] 全部加 v"的 O(1) 标记

一维差分如何实现"区间 [l, r] 全部加 v"的 O(1) 标记?

  • 差分数组
  • 区间更新的 O(1) 标记
  • 差分数组:在原数组上做区间 [l,r] 加 v,标记 l += v、r+1 -= v

对差分数组 d,实现"区间 [l, r] 全部加 v"只需 d[l] += v 且 d[r+1] -= v(两处 O(1) 操作)。原因:diff[l] 加 v 使从 l 开始的前缀和都增加 v,diff[r+1] 减 v 使从 r+1 开始抵消,从而只在 [l, r] 内生效。处理完所有区间更新后,对差分数组做一次前缀和扫描即可得到原数组每点的最终值。

差分把"区间加"从 O(len) 降到 O(1)(只改两端),代价是最后要一次前缀和还原。这是"区间更新 + 区间查询"里更新远多于查询时的经典优化。关键在于 d[l]+=v, d[r+1]-=v 的端点标记。

#
★★

18. 为什么最小覆盖子串收缩时要先判断"该字符仍必需"再移动

为什么最小覆盖子串收缩时要先判断"该字符仍必需"再移动左指针?

  • 精确收缩
  • 必需字符判断
  • 必需字符判断:收缩前先判断该字符是否仍为窗口覆盖所必需,避免误删

收缩窗口时,移除左指针字符可能使窗口不再覆盖 t。若该字符是"必需"的(即移除后其计数低于 need 需求),则 matched 会减少,窗口失效,此时应停止收缩并记录;若该字符"非必需"(移除后仍满足需求),则移除它不影响覆盖,可以继续收缩。因此要先判断"该字符是否仍必需"再决定是否移动,以及是否更新 matched。这保证了收缩只在合法前提下进行,得到最小覆盖窗口。

关键区分"必需"与"冗余":冗余字符(超出需求次数)可以安全移除,不影响窗口覆盖 t;必需字符(达到需求临界)移除会使窗口失效。通过判断"移除后计数是否跌破 need"决定是否更新 matched,精确控制收缩时机,避免多移或漏移。

#
★★

19. 含通配符的子串匹配如何用窗口+频率表处理 '?' 与 '*'

含通配符的子串匹配如何用窗口 + 频率表处理 '?' 与 '*'?

  • 通配符匹配
  • 窗口 + 频率表
  • 通配符处理:用频率表与双指针匹配 '?' 与 '*' 的任意匹配语义

当模式串含 '?'(匹配任意单个字符)和 ''(匹配任意长度)时,若 ' ' 数量有限,可把模式拆成若干由 '' 分隔的"普通段"(每段中间可含 '?'),然后对每段用"窗口 + 频率表"在文本中定位:'?' 视为可匹配任意字符,段的匹配用滑动窗口比较频率(允许 '?' 通配)。'' 作为段间分隔,段间顺序匹配即可。这样把通配符匹配转化为"序列的子串段匹配"。

通配符匹配的难点在 '' 的任意长度;若 '' 少,可拆段处理。每段内用窗口 + 频率表检验('?' 使该位置可任意),段间按顺序用贪心/回溯匹配。若 '*' 很多则退化为动态规划。窗口 + 频率表适合"段内固定长度子串匹配"。

#
★★

20. 字符串排列(567)窗口长度固定时的判定优化

字符串排列(LeetCode 567)窗口长度固定时的判定优化是什么?

  • 定长窗口常量化
  • 频率表比较
  • 定长窗口:长度固定为 K,窗口每移动一次只更新首尾字符频率

判断 s2 是否包含 s1 的某个排列,等价于判断 s2 中是否存在长度为 len(s1) 的窗口,其字符频率与 s1 完全一致。用定长窗口:先统计 s1 的频率和首个窗口的频率,然后窗口每次滑入滑出一个字符,用 O(1) 更新频率,并维护"匹配的字符种类数"变量以 O(1) 判断窗口是否与 s1 频率一致。若匹配数等于 s1 不同字符数则找到排列。整体 O(n)。

优化在于用固定长度窗口 + 频率表 + "匹配字符数"变量,避免每次比较整个频率表。滑入滑出时只更新匹配计数,O(1) 判断是否全匹配。这是"字符串排列"的线性最优解。

#
★★

21. 最长连续 1(限翻转 K 个 0)如何用窗口内 zero 计数控制

最长连续 1(最多翻转 K 个 0)如何用窗口内 zero 计数控制?

  • 窗口内 zero 计数
  • 变长窗口
  • zero 计数:窗口内 0 的个数不超过 K 即合法,控制翻转次数

把"翻转 K 个 0"理解为"窗口内最多允许 K 个 0"。用滑动窗口,右指针右扩,维护窗口内 0 的个数 zero;当 zero > K 时,左指针左缩,若移除的是 0 则 zero--,直到 zero ≤ K。此时窗口内最多 K 个 0,其余全是 1(可被翻转),记录窗口最大长度。整体 O(n)。

关键是把"翻转 K 个 0"转化为"窗口内 0 的个数 ≤ K"这一滑动窗口约束。窗口内 0 到 K 个时,这些 0 都能被翻转为 1,所以窗口长度就是"翻转后连续 1 的长度"。用 zero 计数控制窗口合法性即可。

#
★★

22. 统计所有"恰好 K 个"子串的数量而非仅找最大值

如何统计所有"恰好 K 个"的子串数量,而不仅是找最大值?

  • atMost 差分计数
  • 恰好 K 个计数
  • atMost 差分:atMost(K)-atMost(K-1) 得到恰好 K 个不同字符的子串数

统计"恰好 K 个"的子串数量,用 atMost(K) - atMost(K-1)。atMost(k) 统计"最多 k 个"的子串数,用滑动窗口(右扩时,若窗口内特征数超过 k 则左缩,累加窗口内以当前右端为结尾的合法子串数 r-l+1)。atMost 是单调可滑动的,因此 O(n) 求两个 atMost 相减即得恰好 K 个的数量。这个技巧适用于"恰好 K 个不同字符""恰好 K 个奇数"等。

"恰好"的边界条件不适合滑动窗口直接统计,但"最多"是单调条件可以滑动。atMost(K) - atMost(K-1) 用容斥把"恰好"转化为两个"最多",是子数组计数问题的通用技巧。注意统计时累加的是"以当前右端为结尾的合法子串数"。

#
★★

23. 统计满足"子串中每种字符出现次数≥某阈值"的个数

如何统计满足"子串中每种字符出现次数 ≥ 某阈值"的子串个数?

  • 阈值约束
  • 枚举不同字符集
  • 字符集枚举:枚举不同字符的种类,对每种约束做计数累加

若阈值"≥某值"且涉及不同字符种数,常用技巧是枚举子串包含的"字符种数"(假设取恰好 k 种字符),然后对每个 k 用滑动窗口维护窗口内每种字符的具体计数,并检查是否满足"每种 ≥ 阈值"。由于字符集有限(如 26 个小写字母),可枚举 k 从 1 到 26,每个 k 跑一次 O(n) 滑动窗口,总 O(26 × n)。窗口内用计数数组和"不足阈值"的字符数来判定。

"每种字符出现次数 ≥ 某阈值"是不好直接滑动的约束(窗口大小不规范),但枚举"字符种数 k"后,窗口合法条件变为"恰好 k 种字符且每种 ≥ 阈值",可滑动维护。枚举字符集大小使其可行,这是"枚举字符集"的常见技巧。

#
★★

24. 至少有 K 个重复字符的最长子串如何用分治+计数结合

"至少有 K 个重复字符的最长子串"(LeetCode 395)如何用分治 + 计数结合?

  • 分治思想
  • 计数与拆分
  • 分治拆分:当某字符数量不足阈值时,以其为界拆分区间递归求解

用分治:统计整个字符串中每个字符出现次数,找出出现次数少于 K 的字符(这些字符不能出现在"合法子串"中,因为它们不足 K 次)。以这些"坏字符"为分割点,把字符串拆成多段,每段递归求解,取所有段的最大长度。若某段没有坏字符(所有字符出现 ≥ K 次),则整段合法,返回整段长度。这样分治把问题缩小到不含坏字符的子段,复杂度 O(26 × n) 或 O(n log n)。

关键观察是"出现次数 < K 的字符不能出现在答案中",所以将其作为分割点,把问题分解为多个子问题。分治递归处理各段,段内无坏字符则整段是合法候选。这是"用不满足约束的字符切分"的分治技巧。

#

25. 如何在滑动窗口中维护"不同元素个数/最大频率"两类信息

如何在滑动窗口中维护"不同元素个数 / 最大频率"两类信息?

  • 频率表的双信息
  • 增量维护
  • 双信息维护:同时维护不同元素个数与最大频率,滑动时增量更新

用频率表(数组或哈希)记录每个元素计数,同时维护两个变量:distinct(不同元素个数,当计数从 0 变 1 时 +1,从 1 变 0 时 -1)和 maxFreq(当前最大频率,当某元素计数增大时更新为 max(maxFreq, 新计数);当移除元素时,若被移除元素原来是最大频率,可能需要重新扫描,或用 TreeMap 维护频率分布)。distinct 和 maxFreq 随窗口增量更新。

distinct 只需跨越 0↔1 时更新,O(1)。maxFreq 增加时 O(1) 更新;但减少时(移除最大频率元素)无法直接 O(1) 知道新最大,通常用"频率分布表"(freq→count 的映射)或 TreeMap 来维护,或接受 O(字符集) 的退化。根据需求选择维护方式。

#

26. 对撞指针与快慢指针的语义区分及各自典型场景

对撞指针与快慢指针的语义区分及各自典型场景是什么?

  • 双指针分类
  • 场景区分
  • 指针语义:对撞指针用于有序性/双端归并,快慢指针用于成环/长度检测

对撞指针:两个指针从两端向中间靠拢(left 右移、right 左移),用于有序数组上的"两数之和"、"盛水容器"、"回文判断"等,特点是一次问一次、收敛到中间。快慢指针:两个指针同向移动,速度不同(fast 每次 2 步、slow 每次 1 步),用于链表"找环"、"找中点"、"找倒数第 K 个节点"等。它们语义不同:对撞是"两端逼近",快慢是"同向差速"。

对撞指针利用"有序/边界"信息,关注两端;快慢指针利用"速度差"检测环或定位,多用于链表。区分场景:需要两头比较用对撞,需要检测循环/定位中位用快慢。

#

27. 四数之和如何在外层固定后复用三数之和框架并继续剪枝

四数之和如何在外层固定两个数后复用三数之和框架并继续剪枝?

  • 四数之和转三数之和
  • 剪枝优化
  • 剪枝优化:固定外层后跳过重复值、提前终止不可能解,减少无效枚举

排序后,外层固定第一个数 a,再固定第二个数 b,然后对剩余部分用双指针近似三数之和中"固定一个找两个"的模式(即固定 a 后,内部循环固定 b,再用双指针找 c+d = target - a - b)。复杂度 O(n³)。剪枝:固定 a 时,若 a 与后面最小几个数的和已 > target 则可提前结束;若 a 与最大几个数的和 < target 可跳过;固定 b 后同理;同时用跳过重复值去重。

四数之和 = 排序 + 固定两个数 + 双指针,复用三数之和的"固定 + 双指针"框架,复杂度 O(n³)。剪枝利用排序后"最小/最大和"的边界判断减少无效枚举,去重用跳过重复值。这是"N 数之和"的递推框架。

#

28. 多条件(长度≥L 且不同字符≤K)窗口如何定义合法

多条件(如长度 ≥ L 且不同字符 ≤ K)窗口如何定义合法?

  • 多条件合法
  • 组合判定
  • 组合判定:同时满足长度与字符数两个约束,用两个指标联合判断合法性

把"合法"定义为一个组合条件:窗口需同时满足"长度 ≥ L"和"不同字符 ≤ K"(或其它约束)。用滑动窗口维护窗口内状态(长度、不同字符数),右扩加入元素,当"不同字符 > K"时左缩(这是硬约束,必须恢复);而"长度 ≥ L"是软约束,只在满足时才更新答案。判定函数组合多个布尔条件,硬约束控制收缩、软约束控制记录。这样多条件也能用滑动窗口处理。

区分"硬约束"(必须满足,超了必须收缩,如不同字符 ≤ K)与"软约束"(满足时更新答案,如长度 ≥ L)。合法判定 = 硬约束成立,答案记录 = 软约束也成立。把约束抽象成可组合的判定函数即可应对多条件。

#

29. 如何用"异位词"窗口思想解决变体(允许 K 处不同)

如何用"异位词"窗口思想解决变体问题(如允许 K 处不同)?

  • 异位词窗口
  • 允许差异的变体
  • 允许差异变体:在不完全相同的前提下放宽窗口判定,允许 K 处不同

异位词判定用"窗口频率与目标频率一致"(或"匹配字符数")。变体"允许 K 处不同"则放宽为"窗口中与目标不同的字符数 ≤ K"。用定长窗口 + 频率表,维护窗口内"与目标一致的字符数"或"差异数",当差异数 ≤ K 时满足条件。滑动窗口时 O(1) 更新差异数,即可处理"允许 K 处不同"的变体。

标准异位词要求"差异数 = 0";允许 K 处不同则要求"差异数 ≤ K"。把判定从"完全一致"放宽为"差异 ≤ K",用频率表 O(1) 维护差异计数,滑动窗口即可。这是把"异位词"判定泛化为"近似匹配"。

#

30. 差分在"多次区间更新后求点值"场景相比线段树的优势

差分在"多次区间更新后求点值"场景相比线段树有什么优势?

  • 差分 vs 线段树
  • 复杂度对比
  • 复杂度对比:差分离线 O(n) 优于线段树建树 O(n log n),适合纯累加场景

当只需"多次区间更新 + 最后一次性求每个点的值"(无在线区间查询)时,差分数组优势明显:每次区间更新 O(1),最后一次前缀和 O(n) 还原,总复杂度 O(n + m)(m 次更新)。线段树每次更新 O(log n),且要维护树结构,实现复杂、常数大。若还需要在线查询任意区间和,则线段树更合适。差分在该场景下更简单、更快、更省内存。

差分适合"离线批量更新 + 一次性求点值";线段树适合"在线更新 + 在线查询"。当不需要在线查询时,差分的 O(1) 更新显著优于线段树的 O(log n),且实现简单。这是"按需选择数据结构"的体现。

#

31. 按位运算约束(如 XOR 恰好)的窗口如何维护前缀异或

按位运算约束(如 XOR 恰好为 K)的窗口如何用前缀异或维护?

  • 前缀异或
  • 哈希计数
  • 前缀异或:用 xor 前缀数组维护区间异或,配合哈希做计数

与"前缀和"类似,用前缀异或 pre[i] = pre[i-1] ^ nums[i]。子数组 [l..r] 的异或 = pre[r] ^ pre[l-1](因为异或可逆,a^b^a=b)。要统计"异或和为 K 的子数组",等价于统计 pre[r] ^ pre[l-1] = K,即 pre[l-1] = pre[r] ^ K。用哈希表记录每个前缀异或出现次数,遍历时对每个 r 累加 map[pre[r] ^ K]。整体 O(n)。

异或与加法的区别是"可逆性"相同(a^a=0),所以前缀异或与函数关系和前缀和完全同构,可用同一套哈希计数思路。只需把"和"换成"异或"、把"减"换成"异或"。这体现了"异或前缀和"与"加法前缀和"的统一。

#

32. 窗口内维护"元素种类数"与"每种计数"的两张表如何协同

窗口内维护"元素种类数"与"每种计数"的两张表如何协同?

  • 双表协同
  • 增量维护
  • 双表协同:一张表计各字符频率、一张表计种类数,增删时同步更新

用一张计数表(map 或数组)记录"每种元素出现次数",用另一张"种类数"(一个整数 distinct)记录当前窗口不同元素个数。协同规则:加入元素时 cnt[x]++,若从 0 变 1 则 distinct++;移除元素时 cnt[x]--,若从 1 变 0 则 distinct--(并删除该键)。两张表通过"cnt 跨 0↔1 边界才改 distinct"来保持同步、彼此一致。

计数表是"细粒度",种类数是"汇总",两者通过 cnt 的 0↔1 边界同步。这样"窗口内不同元素个数"的查询 O(1),同时每种元素计数也可 O(1) 更新。这是滑动窗口"双信息"维护的标准做法。