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

277 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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` 时,按以下流程推导:
```mermaid
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 │
│ │
└──────────────────────────────────────────────────┘
```