Files
leetcode-go/note/2026-05-15/02-SliceIntervalReference.md
T

8.7 KiB
Raw Blame History

tags, create time
tags create time
Go
算法技巧
索引
区间
避坑
2026-05-15 15:30

02-SliceIntervalReference - Go 切片区间与索引操作速查

核心原则:Go 切片是左闭右开 [start, end)

这是所有索引操作的根基。理解这一点后,所有 +1/-1 问题都可以推导而非死记。

s := []int{3, 1, 4, 1, 5, 9, 2, 6}
//    索引:  0  1  2  3  4  5  6  7

s[2:5]   // [4, 1, 5]   → 包含 start(2),不包含 end(5)
//   ^-------^
//  [2, 5)  ← 左闭右开区间

一、三大提取模式

[!tip] 🔑 一句话规则

你手里有 start+length 时用 s[start : start+len];你手里有 start+end 时用 s[start : end]。

场景 表达式 示例
已知起始位置和长度 s[start : start+len] s[2 : 2+3] = s[2:5]
已知起止索引(均含) s[left : right+1] 窗口 [left, right] → s[left:right+1]
已知起止索引(右不含) s[start : end] s[0 : len(s)] = 整个切片
从末尾倒数 N 个 s[len(s)-N :] s[len(s)-3:] = 最后 3 个

逐步验证

// 假设 left=2, right=4(表示索引 2~4,共 3 个元素)
left := 2
right := 4
s[left : right+1]   // s[2:5] → [4, 1, 5] ✅
// ↑                     ↑         ↑
// left 保留              right+1    结果恰好是索引 2,3,4 三个元素
//                        (因为右端点不可取)

[!question] 💡 为什么是 right+1 而不是 right?

思考题:如果直接写 s[left:right],实际截取的是索引 left 到 right-1——少了最后一个元素。

答案:因为 Go 切片是左闭右开 [start, end),当算法中的区间定义为闭区间 [left, right](左右都包含)时,切片的第二个参数必须加 1 才能覆盖到 right 这个位置。


二、循环边界规律

基本模式

// 遍历索引 0 ~ n-1(共 n 个元素)
for i := 0; i < n; i++ {      // ✅ 标准:严格小于
    _ = s[i]
}

// 遍历闭区间 [left, right](共 right-left+1 个元素)
for i := left; i <= right; i++ {  // ✅ 闭合区间用 <=
    _ = s[i]
}

// 遍历半开区间 [left, right)(共 right-left 个元素)
for i := left; i < right; i++ {  // ✅ 半开区间用 <
    _ = s[i]
}

滑动窗口中常见的两种循环写法

// 写法 A:右边界 for-range(最常用)
for right, char := range s {       // right ∈ [0, len(s))
    // ...
}

// 写法 B:双指针手动控制
for left := 0; left < len(s); {
    // ...
    left++                          // 注意:只有 ++ 没有 i--
}

// 写法 C:内外层收缩
for right := 0; right < len(s); right++ {
    // expand
    for formed == required {
        // shrink
        left++                      // left 永远单调递增,不会越界
    }
}

三、常见场景速查表 ⭐

场景 1:子串/子数组

// ── 提取窗口内容 ─────────────────────
bestStart := 2
bestLen := 3
result := s[bestStart : bestStart+bestLen]
//          ^^^^^^^^^^            ^^^^^^^^^^^
//          起始位置               结束位置=start+len

// ── 提取固定区间 [left, right] ─────
left, right := 2, 4
window := s[left : right+1]       // +1 是因为右端要包含 right
//                    ^^^

场景 2:二分查找

func search(nums []int, target int) int {
    left, right := 0, len(nums)-1     // 闭区间 [0, n-1]

    for left <= right {                // 闭区间 → <=
        mid := left + (right-left)/2
        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1             // 排除 mid,+1
        } else {
            right = mid - 1            // 排除 mid,-1
        }
    }
    return -1
}

[!warning] ⚠️ 二分查找的关键对应关系

区间定义 while 条件 左移 右移
[left, right] 闭区间 left <= right left = mid + 1 right = mid - 1
[left, right) 半开区间 left < right left = mid + 1 right = mid

场景 3:前缀和

pref := make([]int, n+1)   // 比原数组多一位!
for i := 0; i < n; i++ {
    pref[i+1] = pref[i] + arr[i]
}

// 区间 [l, r] 的和 = pref[r+1] - pref[l]
sum := pref[r+1] - pref[l]
//            ^^^  ← +1:前缀和的索引比原数组大一位

[!tip] 🔑 记忆口诀:前缀和多一位,求和用 r+1 减 l。

场景 4:差分数组

diff := make([]int, n+1)   // 同样需要多一位

// 区间 [l, r] 统一加 v
diff[l] += v
diff[r+1] -= v              // +1:r 之后才恢复

// 还原原数组
for i := 1; i < n; i++ {
    diff[i] += diff[i-1]
}

场景 5:矩阵坐标

// 行 [r1, r2], 列 [c1, c2](均闭区间)
for r := r1; r <= r2; r++ {
    for c := c1; c <= c2; c++ {
        _ = matrix[r][c]    // 访问合法
    }
}

// 提取子矩阵
sub := make([][]int, r2-r1+1)
for i := 0; i < len(sub); i++ {
    sub[i] = matrix[r1+i][c1 : c2+1]   // 列方向也要 +1
}

四、常见陷阱对照表

错误写法 正确写法 问题说明
s[start:end-1] s[start:end] 少了最后一个元素(本来就不该取 end)
s[left:right-1] s[left:right+1] 闭区间 [left,right] 需转半开 [left, right+1)
for i<=n; i++ for i<n; i++ s[n] 越界(索引最大为 n-1)
mid = (left+right)/2 mid = left+(right-left)/2 left+right 可能溢出(虽然 Go 不溢出但这是习惯)
if mid == target 后用 break 直接 return mid break 只跳出内层循环,漏了后续逻辑
diff[r] -= v diff[r+1] -= v 差分应在区间结束后恢复,不是结束时
prefix[r]-prefix[l-1] prefix[r+1]-prefix[l] 如果用 1-indexed 前缀和,公式不同

五、思维模型:从区间语义推导表达式

遇到不确定的 +1/-1 时,按以下流程推导:

flowchart TD
    A["确定区间语义"] --> B{"区间定义方式?"}
    B -->|"闭区间 [l, r]"| C["两端都包含"]
    B -->|"半开区间 [l, r)"| D["左包含,右不包含"]
    C --> E["切片需要覆盖 r<br/>→ 第二个参数 = r+1"]
    D --> F["第二个参数本身就是 r<br/>→ 直接用 s[l:r]"]
    E --> G["for 循环用 l <= r"]
    F --> H["for 循环用 l < r"]
    G --> I["写出代码<br/>用小规模样例验证"]
    H --> I
    I --> J["✅ 确认无误"]

实例演练

题目:找到最长无重复子串的长度
状态:left=2, right=5(当前窗口覆盖索引 2~5)

问:如何提取这段子串?

推导步骤:
1. 区间定义:[left, right] = [2, 5] → 闭区间,包含两端
2. Go 切片语法:[start, end) → 第二个参数不能取到
3. 所以 end = right + 1 = 6
4. 结果:s[2:6] → 覆盖索引 2,3,4,5 ✅

验证:s[2:6] 长度 = 6-2 = 4,而 [2,5] 元素个数 = 5-2+1 = 4 ✓

六、速查卡片 🃏

[!success] ✅ 一页搞定

┌──────────────────────────────────────────────────┐
│           Go 切片 +1/-1 速查卡                    │
├──────────────────────────────────────────────────┤
│                                                  │
│  已知 start + len  →  s[start : start+len]      │
│  已知 [left, right] → s[left : right+1]          │
│  已知 [start, end)  →  s[start : end]            │
│                                                  │
│  闭区间 for → i <= r                              │
│  半开区间 for → i < r                             │
│                                                  │
│  二分 [l,r]    → while l<=r, l=mid+1, r=mid-1    │
│  二分 [l,r)    → while l<r,  l=mid+1, r=mid      │
│                                                  │
│  前缀和 [l,r]  → pref[r+1] - pref[l]             │
│  差分数 [l,r]  → diff[l]+=v, diff[r+1]-=v       │
│                                                  │
│  最后一位       →  s[len(s)-1]                   │
│  最后 N 位      →  s[len(s)-N :]                 │
│                                                  │
│  总原则:切片永远少一位,闭区间要 +1               │
│                                                  │
└──────────────────────────────────────────────────┘