← 返回题目列表

恰好 K 个奇数的子数组数量,为什么可以转成前缀奇数个数?

高频 中等 第 10 / 20 题 更新于 2026/08/03
前缀和哈希计数子数组

简化版

恰好 K 个奇数的子数组,可以把奇数记为 1、偶数记为 0

问题就变成二进制数组中和为 K 的子数组个数。

遍历时维护奇数前缀个数 oddCount,答案增加之前出现过的 oddCount - k 次数。

详细版

奇偶性比具体数值更重要。

遇到奇数时,当前前缀奇数数加 1;遇到偶数则不变。如果当前位置的前缀奇数数为 oddCount,那么要让某段子数组恰好有 k 个奇数,左侧前缀应该是 oddCount - k

哈希表记录每个前缀奇数数出现的次数。

初始化 count[0] = 1,表示空前缀。时间复杂度 O(n),空间复杂度 O(n);也可以用“至多 K 个奇数 - 至多 K-1 个奇数”的滑动窗口做法。

完整版教学

一、为什么只看奇偶性

题目问的是“奇数个数”,不是子数组元素和。一个奇数贡献 1 个目标单位,一个偶数贡献 0 个目标单位。因此可以把原数组映射成 0/1 数组:奇数为 1,偶数为 0。这样恰好 K 个奇数,就等价于映射数组的区间和为 K。

记忆钩子:统计某类元素出现 K 次,可以把属于该类记为 1,不属于记为 0。

二、前缀奇数个数的公式

prefixOdd[j] 是到位置 j 的奇数累计数。如果区间 (i, j] 中有 k 个奇数,则:

prefixOdd[j] - prefixOdd[i] = k
prefixOdd[i] = prefixOdd[j] - k

所以遍历到当前位置时,只需要知道之前有多少个前缀奇数数等于 oddCount - k

三、带数字推演

数组 [1,1,2,1,1]k = 3。映射后是 [1,1,0,1,1]

初始 count[0]=1
i=0 odd=1,找 -2,无
i=1 odd=2,找 -1,无
i=2 odd=2,找 -1,无
i=3 odd=3,找 0,有 1 个,ans=1
i=4 odd=4,找 1,有 1 个,ans=2

答案是 2,对应 [1,1,2,1][1,2,1,1]

四、代码模板

实现如下:

function numberOfSubarrays(nums, k) {
  const count = new Map([[0, 1]])
  let oddCount = 0
  let ans = 0
  for (const x of nums) {
    if (x % 2 !== 0) oddCount++
    ans += count.get(oddCount - k) || 0
    count.set(oddCount, (count.get(oddCount) || 0) + 1)
  }
  return ans
}

哈希表存次数,因为同一个前缀奇数数可能在多个偶数位置重复出现,这些重复都会形成不同子数组。

五、和滑动窗口解法的关系

因为映射后是非负数组,也可以统计“最多 K 个奇数”的子数组数量,再相减得到恰好 K 个:

exactly(k) = atMost(k) - atMost(k - 1)
方法依赖条件优点
前缀和哈希通用计数公式直接表达恰好 K
滑动窗口相减非负贡献空间可做到 O(1)

两种方法都值得掌握。

六、常见误区与追问

  • 误区:用元素值求和。 本题只关心奇数数量,偶数具体大小没有意义。
  • 误区:哈希表存第一次位置。 求个数要存出现次数,不是存位置。
  • 误区:忘记空前缀。 从下标 0 开始的漂亮子数组会漏掉。
  • 追问:偶数有什么作用? 偶数不增加奇数数,但会让相同前缀状态重复,从而增加组合数量。
  • 追问:能否用滑动窗口? 可以,用至多 K 减至多 K-1,因为奇数贡献是非负的。

这些点说明你是否会把计数条件转成前缀状态。

七、加强记忆

恰好 K 个奇数记成“奇数变 1,偶数变 0,找和为 K”。遍历时维护 oddCount,查 oddCount-k 出现过几次。因为要统计数量,哈希表存次数;因为可能从开头开始,初始化 0 出现 1 次。偶数虽然贡献 0,但它会制造更多相同前缀,别忽略它。