--- tags: ["Go", "算法技巧", "索引", "区间", "避坑"] create time: 2026-05-15 15:30 --- # 02-SliceIntervalReference - Go 切片区间与索引操作速查 ## 核心原则:Go 切片是左闭右开 `[start, end)` 这是所有索引操作的根基。理解这一点后,所有 `+1/-1` 问题都可以**推导而非死记**。 ```go 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 个 | ### 逐步验证 ```go // 假设 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` 这个位置。 --- ## 二、循环边界规律 ### 基本模式 ```go // 遍历索引 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] } ``` ### 滑动窗口中常见的两种循环写法 ```go // 写法 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:子串/子数组 ```go // ── 提取窗口内容 ───────────────────── 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:二分查找 ```go 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:前缀和 ```go 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:差分数组 ```go 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:矩阵坐标 ```go // 行 [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 B{"区间定义方式?"} B -->|"闭区间 [l, r]"| C["两端都包含"] B -->|"半开区间 [l, r)"| D["左包含,右不包含"] C --> E["切片需要覆盖 r
→ 第二个参数 = r+1"] D --> F["第二个参数本身就是 r
→ 直接用 s[l:r]"] E --> G["for 循环用 l <= r"] F --> H["for 循环用 l < r"] G --> I["写出代码
用小规模样例验证"] 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