0 和 1 数量相等的最长子数组,为什么把 0 看成 -1?
简化版
把数组里的 0 看成 -1,1 仍然看成 1。
这样一个子数组中 0 和 1 数量相等,就等价于这段转换后的和为 0。
遍历时维护前缀和,记录每个前缀和第一次出现的位置;如果同一个前缀和再次出现,中间这段和为 0,更新最长长度。
详细版
这题的关键转化是平衡计数。
遇到 1,前缀和加 1;遇到 0,前缀和减 1。如果两个位置的前缀和相同,说明它们之间的增减量抵消,也就是 0 和 1 数量相等。
为了得到最长长度,哈希表里应该保存某个前缀和第一次出现的位置。再次遇到相同前缀和时,用当前位置减第一次位置。
初始化 map.set(0, -1),表示从开头到当前位置如果前缀和为 0,也是一段合法子数组。
时间复杂度 O(n),空间复杂度 O(n)。
完整版教学
一、为什么要把 0 转成 -1
原数组只有 0 和 1,直接求和只能知道 1 的数量,不能同时反映 0 的数量。把 0 当成 -1 后,子数组和等于 0 就表示 1 的贡献和 0 的负贡献刚好抵消。这样“数量相等”就被转换成“区间和为 0”。
记忆钩子:平衡类 0/1 问题,把 0 变成 -1,平衡就变成和为 0。
二、同前缀和为什么表示中间和为 0
设 prefix[i] 是到当前位置的累计平衡值。如果 prefix[j] == prefix[i],那么 (i, j] 这一段的增量为:
prefix[j] - prefix[i] = 0
增量为 0,说明这一段里的 1 和 0 数量相同。这个结论不依赖段内顺序,只看总贡献。
三、为什么记录第一次出现的位置
题目要最长子数组。同一个前缀和出现多次时,当前位置固定,越早的出现位置能形成越长的区间。因此哈希表里只在第一次看到某个前缀和时记录位置,后面不要覆盖。
| 前缀和 | 第一次位置 | 再次位置 | 长度 |
|---|---|---|---|
| 0 | -1 | 5 | 6 |
| 1 | 1 | 4 | 3 |
保留最早位置,才能得到最长答案。
四、带数字推演
数组 [0,1,0,1,1] 转换成 [-1,1,-1,1,1]。
初始 sum=0 at -1
i=0: sum=-1,记录 -1 -> 0
i=1: sum=0,之前在 -1,长度 2
i=2: sum=-1,之前在 0,长度 2
i=3: sum=0,之前在 -1,长度 4
i=4: sum=1,记录 1 -> 4
最长长度是 4,对应 [0,1,0,1]。
五、代码模板
实现如下:
function findMaxLength(nums) {
const firstIndex = new Map()
firstIndex.set(0, -1)
let sum = 0
let ans = 0
for (let i = 0; i < nums.length; i++) {
sum += nums[i] === 1 ? 1 : -1
if (firstIndex.has(sum)) {
ans = Math.max(ans, i - firstIndex.get(sum))
} else {
firstIndex.set(sum, i)
}
}
return ans
}
初始化 0 -> -1 是为了处理从下标 0 开始就平衡的情况。
六、常见误区与追问
- 误区:只统计 1 的个数。 题目要求 0 和 1 数量相等,必须同时表达两者差值。
- 误区:重复前缀和时覆盖位置。 覆盖会丢掉最早位置,导致最长长度变短。
- 误区:忘记初始化 0 到 -1。 从数组开头开始的合法子数组会被漏掉。
- 追问:为什么不是滑动窗口? 数组转换后有正有负,窗口和不具备单调性。
- 追问:能扩展到其他平衡字符吗? 可以,核心是把不同类别映射成可抵消的状态,但多类别通常要用向量状态。
这些点考的是“平衡转前缀状态”的能力。
七、加强记忆
这题记成“0 变 -1,同和夹出平衡段”。前缀和相同表示中间净增量为 0,也就是 0 和 1 数量抵消。为了最长,哈希表只保留第一次出现的位置。初始化 sum=0 在 -1,可以自然覆盖从开头开始的答案。