1. 算法学习笔记:2026年3月24日记录
今天是我系统学习算法的第137天。从最初连冒泡排序都要反复调试的菜鸟,到现在能独立解决LeetCode中等难度问题,这个过程让我深刻体会到算法不仅是面试的敲门砖,更是培养计算思维的绝佳途径。最近在刷题时发现,很多看似不同的题目背后都藏着相似的解题模式,这促使我开始用更系统的方式整理学习心得。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 今日重点算法类型解析
2.1 动态规划实战:最长公共子序列变种
今天重点攻克了LCS(最长公共子序列)的三种变种问题。经典LCS问题中,我们通常用dp[i][j]表示text1前i个字符与text2前j个字符的最长公共子序列长度。但在实际面试中,更常遇到的是需要输出具体子序列、计算所有可能子序列数量等变种。
以LeetCode 1143的扩展为例,当需要返回具体子序列时,我们可以通过反向追踪dp表来实现:
python复制def longestCommonSubsequence(text1, text2):
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# 反向构建结果
result = []
i, j = m, n
while i > 0 and j > 0:
if text1[i-1] == text2[j-1]:
result.append(text1[i-1])
i -= 1
j -= 1
elif dp[i-1][j] > dp[i][j-1]:
i -= 1
else:
j -= 1
return ''.join(reversed(result))
这个实现中有几个关键点需要注意:
- dp表的初始化要预留第0行和第0列作为边界条件
- 反向追踪时要注意字符相等的判断条件
- 最终结果需要反转是因为我们是从后往前构建的
2.2 贪心算法的适用场景辨析
很多同学容易混淆动态规划和贪心算法的使用场景。今天在解决"跳跃游戏"问题时,我总结出一个简单的判断标准:
当问题满足"无后效性"且"局部最优能导致全局最优"时,贪心算法往往更高效。以LeetCode 55为例:
python复制def canJump(nums):
max_reach = 0
for i in range(len(nums)):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
return True
这个实现的时间复杂度是O(n),比动态规划的O(n^2)更优。贪心算法在这里有效的关键原因是:只要最远能到达的位置≥当前索引,就说明这个位置是可达的,后续的决策不需要考虑之前是如何到达这个位置的。
3. 算法优化技巧实录
3.1 空间复杂度的降维打击
在解决二维动态规划问题时,我经常发现dp表实际上只需要前一行或前一列的数据。这时可以通过滚动数组来优化空间复杂度。以经典的01背包问题为例:
原始二维DP:
python复制dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
优化为一维DP:
python复制dp[j] = max(dp[j], dp[j - w[i]] + v[i])
关键点在于内层循环需要从后往前遍历,避免覆盖还未使用的前一轮数据。这种优化可以将空间复杂度从O(nW)降到O(W)。
3.2 剪枝策略在回溯算法中的应用
今天在解决组合总和问题时,我深刻体会到剪枝的重要性。以LeetCode 39为例,原始的回溯解法可能会重复计算很多无效路径。通过排序+剪枝可以大幅提升效率:
python复制def combinationSum(candidates, target):
candidates.sort()
res = []
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # 剪枝
path.append(candidates[i])
backtrack(i, path, remaining - candidates[i])
path.pop()
backtrack(0, [], target)
return res
这里的剪枝条件if candidates[i] > remaining: break能够提前终止不可能得到解的分支。实测显示,在target=100的测试用例中,剪枝后的运行时间从1200ms降到了80ms。
4. 算法学习中的思维误区
4.1 过度追求最优解
刚开始学习算法时,我总想一步到位写出最优解,结果往往连基础解法都写不出来。现在我会采用分步策略:
- 先写出暴力解法,确保逻辑正确
- 分析时间/空间复杂度瓶颈
- 寻找优化方向(记忆化、剪枝、双指针等)
- 逐步优化到满意程度
4.2 忽视边界条件
今天在写二分查找时,又一次被边界条件坑了。我总结了一个检查清单:
- 空输入处理
- 单元素情况
- 重复元素处理
- 目标值不存在的情况
- 整数溢出(特别是mid计算时)
例如标准的二分查找应该这样处理边界:
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
这里使用left <= right而不是left < right是为了正确处理单元素情况,而mid = left + (right - left) // 2可以避免整数溢出。
5. 算法学习资源实践心得
经过几个月的实践,我发现单纯刷题效果有限,必须配合系统学习。我的当前学习路径是:
- 《算法导论》掌握理论基础
- LeetCode按类型刷题(每天2-3题)
- 参加周赛锻炼实战能力
- 定期复习错题本
特别推荐几个帮助我突破瓶颈的资源:
- LeetCode的"探索"板块按专题学习
- 《编程珠玑》中的算法思维训练
- MIT OpenCourseWare的算法公开课
最近我还发现一个提升效率的技巧:用费曼学习法,尝试向虚拟的"小白"解释算法原理。这个过程常常能暴露出自己理解上的漏洞。
