核心算法套路详解
本文通过**"人话 + 动图式拆解 + 代码模拟"**来讲解最核心的算法套路。
数组类算法
1. 动态规划 (Dynamic Programming, DP)
通俗理解: 它的核心叫"记事本"。
假如你在玩游戏,第 10 关很难打。你打过了第 9 关,把存档保存了。下次打第 10 关失败了,你是从第 1 关重新打,还是直接读取第 9 关的存档?当然是读档!DP 就是把每一步算出来的结果(存档)记在数组里,后面直接用。
经典案例:爬楼梯 (LeetCode 70)
题目: 你要爬 阶楼梯。每次你可以爬 1 或 2 个台阶。有多少种不同的方法可以爬到楼梯顶端?
解题思路模拟:
-
假设你要爬到第 10 层。
-
你最后一步只有两种可能:
- 从第 9 层爬 1 步上来。
- 从第 8 层爬 2 步上来。
-
结论: 想知道爬到 10 层的方法数,只要知道(爬到 9 层的方法数 + 爬到 8 层的方法数)。
-
公式(状态转移方程):
推导过程:
- 第 1 层: 只有 1 种方法 (1)。记为 。
- 第 2 层: 有 2 种方法 (1+1, 2)。记为 。
- 第 3 层: 等于第 2 层 + 第 1 层的方法数。。
- 第 4 层: 等于第 3 层 + 第 2 层的方法数。。 ...一直加到第 N 层。
代码逻辑 (Python):
# dp 数组就是我们的"记事本"
def climbStairs(n):
if n <= 2: return n
# 初始化记事本
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
# 从第 3 层开始算,利用之前的存档
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
2. 双指针 (Two Pointers)
通俗理解: 核心叫**"包抄"**。
就像你在书架上找两本书,它们的厚度加起来要等于 10cm,而且书是按厚度从小到大排好的。你用左手指着最薄的,右手指着最厚的,看加起来是不是 10,不对就移动手指。
经典案例:两数之和 II - 输入有序数组 (LeetCode 167)
题目: 给一个从小到大排序的数组 [2, 7, 11, 15],找到两个数,让它们相加等于 目标值 9。
解题思路模拟:
-
初始状态: 左指针
L指向2(最小),右指针R指向15(最大)。 -
第一轮: 计算
2 + 15 = 17。- 结果
17 > 9(太大了)。 - 思考:
15就算跟最小的2加起来都嫌大,那15肯定没戏了。 - 操作: 右指针
R往左移一位,指向11。
- 结果
-
第二轮: 计算
2 + 11 = 13。- 结果
13 > 9(还是大)。 - 操作: 右指针
R再往左移,指向7。
- 结果
-
第三轮: 计算
2 + 7 = 9。- 结果:
9 == 9。找到了!
- 结果:
代码逻辑:
def twoSum(numbers, target):
left = 0
right = len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left, right] # 找到了
elif current_sum > target:
right -= 1 # 太大了,让右边变小 点
else:
left += 1 # 太小了,让左边变大点
3. 滑动窗口 (Sliding Window)
通俗理解: 核心叫**"毛毛虫伸缩"**。
想象你在吃自助餐,盘子(窗口)只能装这么点东西。你想吃更多,就得把盘子这一头往前伸(进食),如果盘子满了或者不符合规定了,那一头就要吐出来(排出),保持盘子里的东西始终符合要求。
经典案例:长度最小的子数组 (LeetCode 209)
题目: 数组 [2, 3, 1, 2, 4, 3],找到一个连续子数组,它的和 ,并且长度最小。
解题思路模拟:
定义窗口 window,左边界 L,右边界 R。
-
进食 (R 向右移):
[2](sum=2) -> 不够[2, 3](sum=5) -> 不够[2, 3, 1](sum=6) -> 不够[2, 3, 1, 2](sum=8) -> 够了!(>=7)。当前长度是 4。
-
缩窗 (L 向右移,试图优化):
- 既然
[2, 3, 1, 2]已经达标,那我们试试把最左边的2扔掉行不行? - 扔掉
2,变成[3, 1, 2](sum=6) -> 不够了。 - 说明刚才那个长度 4 已经是这一轮的极限了。
- 既然
-
继续进食 (R 继续右移):
R移到4。窗口变成[3, 1, 2, 4](sum=10) -> 够了!
-
再次缩窗 (L 向右移):
- 扔掉左边的
3,变成[1, 2, 4](sum=7) -> 够了! 长度为 3 (比刚才的 4 更短,更新答案)。 - 再扔掉左边的
1,变成[2, 4](sum=6) -> 不够了。 - 停止缩窗,继续 R 往右...
- 扔掉左边的
代码逻辑:
def minSubArrayLen(target, nums):
left = 0
current_sum = 0
min_length = float('inf') # 无穷大
# R 主动向右走 (进食)
for right in range(len(nums)):
current_sum += nums[right]
# 当满足条件时,L 尝试向右缩 (减肥/优化)
while current_sum >= target:
current_len = right - left + 1
min_length = min(min_length, current_len) # 更新最小长度记录
current_sum -= nums[left] # 把左边的吐出来
left += 1 # 左边界收缩
return min_length
树与图类算法
4. 深度优先搜索 (DFS - Depth First Search)
通俗理解: 性格叫**"不撞南墙不回头"**。
想象你在走迷宫。你是一个非常有原则的人:只要前面有路,就一直往深处走。直到走到死胡同(撞墙了),才回头退到上一个路口,换一条路继续走到黑。
- 核心工具: 递归 (Recursion) —— 自己调用自己。
经典案例:二叉树的最大深度 (LeetCode 104)
题目: 给一颗树,算出它最高有多少层。
解题思路模拟:
你可以把自己想象成一个只有**"微观视角"**的机器人,你站在任何一个节点上,只知道一件事:
- "我的深度 =
1(我自己) +max(左边孩子的深度, 右边孩子的深度)"
推导过程:
- 根节点问: "我多深?" 先得问左孩子和右孩子。
- 左孩子问: "我多深?" 它下面没孩子了(死胡同,Base Case)。
- 左孩子答: "我是叶子,深度是 1"。汇报给根节点。
- 右孩子继续往下问... 直到所有死胡同都探完。
- 根节点总结: 左边深还是右边深?挑个大的 。
代码逻辑:
def maxDepth(root):
# 1. 死胡同(Base Case):如果没有节点了,深度就是 0
if not root:
return 0
# 2. 递归(一直往下问):
left_depth = maxDepth(root.left) # 问左边
right_depth = maxDepth(root.right) # 问右边
# 3. 总结
return max(left_depth, right_depth) + 1
5. 广度优先搜索 (BFS - Breadth First Search)
通俗理解: 性格叫**"地毯式搜索"**。
这次你不再是一条路走到黑了。想象往平静的湖里扔一颗石头,涟漪是一圈一圈向外扩散的。BFS 就是先看完离我最近的一圈,再看稍微远一点的一圈,层层推进。
- 核心工具: 队列 (Queue) —— 先进先出,像排队买票。
经典案例:二叉树的层序遍历 (LeetCode 102)
题目: 给一颗树,把每一层的节点数字打印出来。比如 [[3], [9, 20], [15, 7]]。
解题思路模拟:
我们要用一个队列 (Queue) 来模拟"排队"。
-
初始: 只有
根节点在排队。 -
第一轮:
根节点出队(处理它)。- 关键点: 根节点离开前,把它的左孩子和右孩子