数组题为什么容易出现越界和 off-by-one 错误?
简化版
数组下标通常从 0 到 n-1,循环边界、区间开闭和空数组处理稍不统一就会越界或漏元素。写数组题最好固定使用左闭右开 [l, r) 思维,循环用 i < n,并单独检查空数组、单元素、首尾位置。
详细版
off-by-one 是“差 1 位”的边界错误,例如把 i <= n 写成遍历数组,最后会访问 a[n] 越界;或者二分里混用 [l,r] 和 [l,r),导致死循环或漏查。
for (int i = 0; i < n; i++) {
// 合法访问 a[i]
}
面试中不仅要能写对,还要能解释区间含义。推荐把数组区间记成 [start, end),长度就是 end - start,空区间是 start == end,切片和遍历都更统一。
完整版教学
一、数组合法下标是 0 到 n-1
长度为 n 的数组,合法下标不是 1 到 n,而是 0 到 n-1。
n = 5
index: 0 1 2 3 4
value: a b c d e
所以遍历条件通常是 i < n。如果写成 i <= n,最后一次会访问 a[n],越过最后一个元素。
记忆钩子:长度是 n,最后一个下标是 n-1。
二、off-by-one 是差一位的错误
off-by-one 常见于循环、切片、窗口、二分、前缀和。它不一定每次崩溃,有时只是漏掉第一个或最后一个元素。
错误例子:
for (int i = 0; i <= n; i++) {
sum += a[i]; // i == n 时越界
}
漏元素例子:
for (int i = 1; i < n; i++) {
sum += a[i]; // 如果没有刻意跳过 a[0],这里漏了首元素
}
面试写代码时,边界错误比思路错误更常见。
三、左闭右开区间更稳定
推荐用 [l, r) 表示区间包含 l,不包含 r。
[0, n) 包含 0..n-1
长度 = n - 0 = n
[l, r) 长度 = r - l
空区间 = l == r
这和很多语言切片一致:
a[start:end] -> 包含 start,不包含 end
当你坚持一种区间模型,循环条件和长度计算会更统一。
四、闭区间也能用,但不能混
二分查找常见两种写法:闭区间 [l, r] 和左闭右开 [l, r)。两者都可以,但更新规则不同。
| 区间模型 | 初始值 | 循环条件 | 丢弃左半 |
|---|---|---|---|
[l, r] | l=0,r=n-1 | l <= r | l = mid + 1 |
[l, r) | l=0,r=n | l < r | l = mid + 1 或 l = mid 视问题 |
最危险的是初始用 [l,r],循环又按 [l,r) 更新,结果不是死循环就是漏答案。
五、空数组和单元素是必测样例
很多数组题在正常数据上能跑,空数组或单元素就崩。
[]
[7]
[1, 2]
[2, 1]
例如访问 a[0] 前要确认 n > 0。双指针题中,l < r 和 l <= r 的选择也要用单元素样例验证。
边界样例不只是测试,它能倒逼你确认区间含义。
六、前缀和里的 n+1 也来自边界统一
前缀和常用长度 n+1 的数组:
prefix[0] = 0
prefix[i+1] = prefix[i] + a[i]
sum [l, r) = prefix[r] - prefix[l]
如果 a = [2, 4, 6]:
prefix = [0, 2, 6, 12]
sum [1, 3) = prefix[3] - prefix[1] = 12 - 2 = 10
这个设计让空前缀和区间求和都统一,减少特殊分支。
七、常见误区与追问
- 误区:循环到
i <= n才能覆盖 n 个元素。 0 基数组覆盖 n 个元素应是i < n。 - 误区:区间开闭随便写,最后调一下就行。 混用区间模型会导致死循环、漏元素和越界。
- 误区:空数组不用单独考虑。 访问首元素、尾元素、初始化答案时都可能在空数组崩溃。
- 追问:为什么推荐
[l,r)? 长度是r-l,空区间是l==r,和切片、前缀和更统一。 - 追问:二分为什么容易死循环? 区间含义和边界更新不匹配,
mid没有排除掉。 - 追问:怎么快速自测边界? 用空数组、单元素、双元素、目标在首尾、不存在这些样例。
八、加强记忆
数组边界题记成“0 到 n-1,区间要统一”。遍历用 i < n,区间尽量用 [l,r),长度就是 r-l,空区间就是 l==r。写完数组题一定拿空数组、单元素、双元素、首尾命中、不存在这 5 类样例过一遍,很多 off-by-one 错误会立刻露出来。