1900 字
10 分钟
力扣算法部分的一些笔记
双指针
双指针主要分为三大门派
1. 对撞指针(左右指针)
适用场景:主要用于有序数组或字符串。常见于查找两数之和、反转字符串、判断回文串、以及像“盛最多水的容器”这种寻找边界极值的问题。
func collisionPointers(nums []int) { left := 0 right := len(nums) - 1
for left < right { // 根据题意,有时可能是 left <= right // 1. 计算当前左右指针组合的状态/结果 // ...
// 2. 根据状态决定移动哪个指针 if /* 需要更大的值 */ { left++ } else if /* 需要更小的值 */ { right-- } else { // 找到目标,执行后续逻辑(如记录结果、去重或退出) // left++ // right-- } }}2. 快慢指针
应用场景:处理链表问题(找中点、判断环)、数组原地去重、移除/移动特定元素(如“移动零”)。
func fastSlowPointers(nums []int) int { slow := 0
for fast := 0; fast < len(nums); fast++ { if /* 满足特定条件,需要保留当前 fast 指向的元素 */ { // 将 fast 的值赋给 slow(或进行交换) nums[slow] = nums[fast] slow++ // slow 推进,准备接收下一个有效元素 } } // 返回有效元素的长度或最终 slow 的位置 return slow}3. 同向指针(在下面滑动窗口说明)
滑动窗口
适用场景: “连续子数组/子串” + 最值/满足条件
像「最大/最长」、「最小/最短」、「刚好满足某个条件(如和为 K,包含特定字符等)」
注意:
-
如果是“子序列”(Subsequence),通常不用滑动窗口,因为子序列不要求连续。
-
注意是否有负数,如果数组包含负数,滑动窗口通常会失效(因为窗口扩大时和不一定增加)
滑动窗口主要分为两类:可变窗口(求最长/最短)和固定窗口。
1. 可变窗口(最通用)
核心思想:right 主动扩张探索,left 被动收缩调整。
func slidingWindowVariable(s string) int { // 1. 定义数据结构维护窗口内的状态 (通常是 map, slice, 计数器等) window := make(map[byte]int) left, right := 0, 0 res := 0 // 或者 math.MaxInt32,取决于求最长还是最短
for right < len(s) { // 2. 将 s[right] 加入窗口,更新状态 c := s[right] window[c]++
// 3. 判断当前窗口状态是否触发了“收缩”条件 for /* 满足收缩条件 (例如窗口内出现了重复字符,或者和超出了限制) */ { // (可选) 在此处更新求【最短】子串的答案
// 4. 准备将 s[left] 移出窗口,更新状态 d := s[left] window[d]-- left++ // left 右移,窗口收缩 }
// 5. 在此处更新求【最长】子串的答案 (因为此时窗口一定满足条件) // res = max(res, right - left + 1)
right++ // right 右移,窗口继续扩张 } return res}2. 固定窗口
核心思想:窗口大小恒定为 k,每次 right 进一个,left 必须出一个。
func slidingWindowFixed(nums []int, k int) int { left, right := 0, 0 // 定义状态变量...
for right < len(nums) { // 1. 将 nums[right] 加入窗口,更新状态
// 2. 如果窗口长度达到 k,说明窗口已经成型 if right - left + 1 == k { // 3. 记录或更新答案
// 4. 将 nums[left] 移出窗口,为下一个新元素腾位置 left++ } right++ } return res}前缀和
适用场景:
-
“连续子数组” + 求和/积
-
区间查询
[i, j] 内的元素之和,就是两个连续子数组相减
-
连续子数组的“特定数学关系”
像子数组和等于 K…
注意
为了避免处理边界条件
➕:在最前面补一个 0,即前缀和数组的长度为原数组长度 + 1
✖️:令第一个为1
回溯
二分查找
本质: 二分查找的本质不是“全局有序”,而是“两段性”。只要能找到一个判断条件,将当前的搜索空间一分为二(一半满足条件,另一半必定不满足),就可以使用二分。
时间复杂度: O(log n)。
根据题型特点,二分查找主要分为以下四种场景:
1. 标准精确查找
- 适用场景: 数组全局有序,且元素不重复。要求寻找某个确切的
target。
2. 寻找左右边界
- 适用场景: 数组全局有序,但包含重复元素。要求寻找
target第一次出现(左边界)或最后一次出现(右边界)的位置。(如 LeetCode 34) - 特征: 找到
target时不停手,而是记录当前位置,并强行收缩另一边的区间,继续向目标边界逼近。
func binarySearchTemplate1(nums []int, target int) int { left, right := 0, len(nums)-1 // 可选:ans := -1 // 如果是找边界/找极值,用外部变量记录最近一次符合条件的答案
for left <= right { mid := left + (right - left)/2
if nums[mid] == target { return mid // 如果是找左边界:ans = mid; right = mid - 1 // 如果是找右边界:ans = mid; left = mid + 1 } else if nums[mid] < target { left = mid + 1 // target 在右侧,砍掉左半边 } else { right = mid - 1 // target 在左侧,砍掉右半边 } }
return -1 // 或 return ans}3. 寻找极值(非全局有序)
- 适用场景: 数组“局部有序”(如旋转数组)或者存在“高低起伏”(如寻找峰值)。(如 LeetCode 153、33)
- 特征: 通常没有明确的
target来匹配,而是通过比较nums[mid]与nums[left]或nums[right]的相对大小,判断出哪里发生了“断层”或“转折”,从而决定收缩方向。
func binarySearchTemplate2(nums []int) int { left, right := 0, len(nums)-1
// 注意:这里绝对不能写等号,否则在某些收缩逻辑下必死循环 for left < right { mid := left + (right - left)/2
if nums[mid] < nums[right] { // 举例:判断条件满足,说明右半边是平稳上坡,极值不可能在右边。 // 但 mid 本身可能是极值,绝不能踢掉! right = mid } else { // 举例:发生了断层跌落,极值必定在 mid 右侧的悬崖下。 // mid 本身处于悬崖上方,已被洗脱嫌疑,果断跨过! left = mid + 1 } }
// 循环结束时,left 和 right 撞在一起,就是那个唯一的极值 return nums[left]}贪心算法
1. 基础匹配
- 适用题目:《分发饼干》(455)
- 技术动作:双数组排序 + 双指针单向匹配。
2. 覆盖范围
-
适用题目:《跳跃游戏 I/II》(55, 45)、《划分字母区间》(763)
-
特征:别管每一步怎么跳,死死盯住“最远能覆盖到哪”。
-
技术动作:
- 维护一个动态的
maxReach(未来最远潜力)和一个currentEnd(当前这一跳的边界)。 - 遍历时不断扩张
maxReach。 - 核心触发器:当
i == currentEnd时,被迫结算(跳跃次数+1,或切分出一段区间),并将边界更新为新的maxReach。
- 维护一个动态的
3. 区间调度
- 适用题目:《合并区间》(56)、《无重叠区间》(435)、《用最少数量的箭引爆气球》(452)
- 通关口诀:求合并看左边界,求消减看右边界
- 技术动作:
- 合并类(如 56 题):按左边界排序。贪心策略是“能吞就吞”,右边界不断取最大值
max(last[1], curr[1])。 - 消减/射箭类(如 435, 452 题):强烈建议按右边界排序!这是降维打击。
- 右边界排序的极简逻辑:谁结束得早,就先安排谁。只要发生重叠(
left <= end),后来的那个必定是个累赘,直接干掉(count++);不重叠才更新安全边界end。
- 合并类(如 56 题):按左边界排序。贪心策略是“能吞就吞”,右边界不断取最大值