diff --git a/note/2026-05-15.md b/note/2026-05-15.md new file mode 100644 index 0000000..e7ba10b --- /dev/null +++ b/note/2026-05-15.md @@ -0,0 +1,12 @@ +--- +tags: [daily] +create time: 2026-05-15 15:30 +--- + +# 2026-05-15 + +## 今日学习 + +- [[01-MaxInt32-initial]] - math.MaxInt32 作为最小值搜索的初始值 +- [[02-SliceIntervalReference]] - Go 切片区间与索引操作速查(左闭右开、+1/-1 推导) +- [[03-StringByteRuneConversion]] - string 索引用 rune() 转换:byte vs rune map key 陷阱 diff --git a/note/2026-05-15/01-MaxInt32-initial.md b/note/2026-05-15/01-MaxInt32-initial.md new file mode 100644 index 0000000..8e46312 --- /dev/null +++ b/note/2026-05-15/01-MaxInt32-initial.md @@ -0,0 +1,74 @@ +--- +tags: ["Go", "算法技巧", "初始化"] +create time: 2026-05-15 15:30 +--- + +# 01-MaxInt32-initial - math.MaxInt32 作为最小值搜索的初始值 + +## 背景 + +在**寻找最小值**的算法中(如最短路径、最短子串、最小距离等),需要用一个"足够大"的值作为初始最优解,确保第一次比较时任何实际值都能将其替换。 + +## Go 中的正确做法 + +```go +import "math" + +bestLen := math.MaxInt32 // 使用整型的最大值作为初始"无穷大" +``` + +### 为什么用 `math.MaxInt32`? + +| 错误做法 | 问题 | +|---------|------| +| `999999` | 不够通用,当答案本身可能很大时会出错 | +| `-1` / `0` | 表示"未找到"而非"无穷大",无法用于 `<` 比较 | +| `math.MaxInt32` | ✅ 语言标准库提供,语义清晰,覆盖所有 int32 范围 | + +## 典型使用场景 + +### 1. 最短覆盖子串 + +```go +bestStart, bestLen := -1, math.MaxInt32 + +for right, char := range s { + // ... expand / shrink logic ... + for formed == required { + currentLen := right - left + 1 + if currentLen < bestLen { // 任何合法长度都 < MaxInt32 + bestStart = left + bestLen = currentLen + } + // ... + } +} +``` + +### 2. 最短路径 / 最小操作数 + +```go +minSteps := math.MaxInt32 +// BFS 或 DP 过程中不断更新 minSteps +if steps < minSteps { + minSteps = steps +} +``` + +## 注意事项 + +> [!warning] ⚠️ 区分"未找到"与"无穷大" + +`math.MaxInt32` 表示的是"当前没有找到可行解",而不是"无解"。判断是否无解需用另一个标记变量(如 `bestStart == -1`): + +```go +if bestStart == -1 { + return "" // 无解 +} +return result[bestStart : bestStart+bestLen] // 有解 +``` + +> [!tip] 🔑 记忆口诀 + +> **找最小 → 初始设最大;找最大 → 初始设最小。** +> 配合一个独立的 found 标记来区分"还没搜到"和"搜到了就是最大/最小"。 diff --git a/note/2026-05-15/02-SliceIntervalReference.md b/note/2026-05-15/02-SliceIntervalReference.md new file mode 100644 index 0000000..bf5f0c3 --- /dev/null +++ b/note/2026-05-15/02-SliceIntervalReference.md @@ -0,0 +1,276 @@ +--- +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 [!danger] ⛔ 没有隐式转换 +> +> Go 和 Python/JavaScript 不同,**byte 不能隐式转为 rune**。即使 byte 的值域完全包含在 rune 内,也必须显式转换: + +```go +// ❌ 编译错误:cannot use leftChar (variable of type byte) as rune value in map index +have[s[left]]-- + +// ✅ 正确:显式转换 +leftChar := rune(s[left]) +have[leftChar]-- + +// ✅ 替代方案:统一用 byte 做 key(仅限 ASCII 场景) +have := make(map[byte]int) +have[s[left]]-- // byte ↔ byte,匹配 ✅ +``` + +## 典型出错场景 + +### 滑动窗口中使用 `range` + `[index]` 混合访问 + +```go +func minWindow(s string, t string) string { + need := make(map[rune]int) // key 是 rune + have := make(map[rune]int) // key 是 rune + + for right, char := range s { // range → rune,与 map key 一致 + have[char]++ // ✅ rune 作为 key + } + + // 但收缩阶段需要按索引访问左边界字符: + leftChar := rune(s[left]) // ← 必须加 rune() 转换! + have[leftChar]-- // ✅ +} +``` + +**根源原因**:`range s` 给出的是完整 Unicode 码点(rune),而 `s[i]` 直接访问的是 Go 字符串的底层字节数组([]byte)。前者一次可能跳过多个字节(对于 UTF-8 多字节字符),后者永远只取一个字节。 + +## byte key vs rune key 如何选择? + +| 场景 | 推荐 key 类型 | 原因 | +|------|-------------|------| +| **仅英文字母 / ASCII** | `map[byte]int` | 少一层转换,性能略优 | +| **含中文 / Emoji / 多字节 Unicode** | `map[rune]int` | 能正确处理多字节字符 | +| **通用安全写法** | `map[rune]int` | 无论输入什么都能工作 | + +> [!tip] 🔑 选择建议 +> +> LeetCode 题目几乎全部只涉及英文字母,所以两种都可以。但养成使用 `rune` 的习惯更安全——代码复用时不会因为遇到非 ASCII 字符而出错。 + +## 常见变体代码对比 + +```go +// ── 错误写法(漏了转换)── +func buggy(s string) { + m := make(map[rune]int) + m[s[0]]++ // ❌ compile error: byte used as rune key +} + +// ── 修正写法 A:加转换 ── +func fixA(s string) { + m := make(map[rune]int) + m[rune(s[0])]++ // ✅ byte → rune 显式转换 +} + +// ── 修正写法 B:改用 byte key ── +func fixB(s string) { + m := make(map[byte]int) + m[s[0]]++ // ✅ byte 对 byte,天然匹配 +} + +// ── 修正写法 C:全用 range,不索引 ── +func fixC(s string) { + m := make(map[rune]int) + for _, c := range s { + m[c]++ // ✅ range 返回 rune,直接用 + } +} +``` + +## 检查清单 + +写涉及 `string` 索引的算法时自问: + +- [ ] 我的 map key 是 `rune` 还是 `byte`? +- [ ] `s[index]` 返回的是 `byte` +- [ ] 如果 key 是 `rune`,是否加了 `rune(s[index])` 转换? +- [ ] 如果使用 `range` 遍历,`char` 本身就是 `rune`,不需要额外转换 + +## 记忆口诀 + +> **`range s` 拿的是 rune,`s[i]` 抠出来是 byte。** +> **rune 地图塞不进 byte,要转就要明说。**