左闭右开区间
· 阅读需 3 分钟
编程时,整数区间使用 [闭, 开) 相较于 [闭, 闭] 有很多优势。
好处都有啥
容易算长度
[2, 5) 的长度为 5 - 2 = 3,差值就是长度,非常直观。
[2, 5] 的长度为 5 - 2 + 1 = 4,容易不小心差 1,很不直观。
容易拼接
[a, b) 和 [b, c) 能直接拼成 [a, c),头尾相接,非常直观。
[a, b] 和 [b + 1, c] 拼成 [a, c] 就有点绕了。
容易表示空区间
[a, a) 是空区间,左 = 右,非常自然。
[a, a - 1] 是空区间,左 > 右,就很别扭。
容易构建区间
既然差值就是长度,那么只要知道起点和长度,就能非常无脑地构建出左闭右开区间 [start, start + length),然后用
for (int i = start; i < start + length; i++) { }
来遍历的子序列,杜绝了一切“差一错误”。
这也是现代编程语言大部分采用从 0 开始的索引的原因。平时最常写的 for 循环
for (int i = 0; i < N; i++) { }
或者
for i in Range(N):
其实就是在遍历 [0, N) 区间,语义非常清晰:“从 0 开始,长度为 N”。
如果改用从 1 开始的索引,就要写丑陋的 [1, N + 1),或者用双闭区间 [1, N],这样就享受不到左闭右开区间的各种好处了。
此事在祖师爷 Dijkstra 的手稿 EWD 831 中亦有记载,当时关于下标应该从 0 还是从 1 开始似乎有不小的争议。
总结
最近刷算法题,尤其是做 76. 最小覆盖子串 时被折磨得不轻,题目逻辑本身已经挺复杂了,如果还要浪费认知资源纠结“算长度要不要 +1”、“用小于还是小于等于”这种边界问题就没完没了了。只有养成良好的习惯,熟练处理边界,才能使注意力回到问题本身,把这种比较复杂的题搞明白。
为了避免各种下标/指针的边界差一问题,尽量使用左闭右开区间,并牢记 [start, start + length) 这个套路吧。