1. 算法设计与分析到底在解决什么问题
很多人第一次接触算法课,脑子里冒出来的画面就是一堆看不懂的数学符号和伪代码,觉得这玩意儿离实际写代码太远。我刚开始也是这个感觉,直到后来做了几个真实项目,回头再看才发现,算法设计与分析解决的是一个非常朴素的问题:同样的计算任务,为什么有的写法跑得飞快,有的写法慢到让人想砸键盘。
举个最直观的例子。假设你要在一个排好序的数组里找一个数,最笨的办法是从头到尾一个一个比对,数组有一万个元素,最坏情况你要比一万次。但如果用二分查找,每次砍掉一半,最多只需要十四次左右就能确定目标在不在。这个差距不是百分之几十,是几百倍的量级。算法设计与分析这门课,本质上就是教你怎么系统性地找到那个“砍一半”的思路,并且证明它确实有效。
北航童老师这门课在圈子里口碑一直不错,原因很简单:它不跟你绕弯子,从最基础的时间复杂度分析开始,一步步带你建立“看到问题先想代价”的思维习惯。这个习惯一旦养成,你写代码的方式会发生根本性变化。以前你可能写完能跑就行,现在你会下意识地琢磨:这个循环能不能提前终止?这个数据结构选得对不对?有没有更优的解法?
适合看这门课的人其实比想象中广。在校学生自不必说,考研复试、机试、课程考试都用得上。已经工作的开发者同样值得花时间过一遍,尤其是做后端、数据处理、推荐系统这类对性能敏感的方向。哪怕你是做前端的,理解算法分析也能帮你写出更高效的DOM操作和状态管理逻辑。这门课不挑语言,伪代码为主,C++、Java、Python都能对应上。
我自己的体会是,算法课最大的价值不在于教你几个具体算法,而在于给你一套分析问题的框架。遇到一个新问题,你会先判断它属于哪一类——是查找、排序、图论、动态规划还是贪心——然后从对应的工具箱里找合适的工具。这个分类和匹配的能力,才是真正拉开程序员水平差距的地方。
2. 课程核心内容拆解与学习路径规划
2.1 复杂度分析:一切讨论的起点
复杂度分析是整门课的地基,但很多人一开始就栽在这里。不是因为它难,而是因为它“看起来简单”,导致很多人草草翻过,后面讲到递归树、主定理的时候就开始懵。
大O记号描述的是算法运行时间随输入规模增长的趋势,不是具体跑多少毫秒。这一点必须刻在脑子里。O(n)和O(n²)的区别,在n等于十的时候可能只差几十毫秒,但n等于一百万的时候,前者可能一秒跑完,后者要跑好几天。这就是为什么我们关心的是增长趋势,而不是常数项。
实际分析的时候,有几个坑我踩过不止一次。第一个坑是只看最外层循环。比如下面这段代码:
for i in range(n): for j in range(i, n): do_something()外层循环n次,内层看起来也是n次,但内层是从i开始的,实际执行次数是n + (n-1) + ... + 1 = n(n+1)/2,所以复杂度是O(n²)。这个还好判断,更隐蔽的是循环内部有函数调用,而那个函数本身还有复杂度,这时候要把它们乘起来。
第二个坑是忽略空间复杂度。有些算法时间上很快,但需要额外开辟和输入规模等大的空间。比如归并排序,时间复杂度稳定在O(n log n),但需要O(n)的辅助空间。如果你在嵌入式设备或者内存受限的环境里工作,这个空间开销可能就是不可接受的。所以分析的时候,时间和空间要同时看。
第三个坑是递归算法的复杂度分析。递归代码看起来简洁,但复杂度往往不好直接看出来。这时候要么画递归树,要么用主定理。递归树适合直观理解,主定理适合快速计算。我的建议是先把递归树画熟,再记主定理的三种情况,这样不容易记混。
注意:复杂度分析里有个常见误区,就是把最好情况和最坏情况搞混。比如快速排序,平均时间复杂度是O(n log n),但最坏情况是O(n²)。面试或者考试的时候,如果题目没特别说明,一般问的是最坏情况,但实际工程中我们更关心平均情况。这个区别要心里有数。
2.2 分治策略:把大问题切成小问题
分治法的核心思想就八个字:分而治之,各个击破。把一个大问题拆成若干个规模更小的同类子问题,递归求解,再把子问题的解合并成原问题的解。听起来简单,但真正用好需要把握三个关键点。
第一个关键点是子问题必须相互独立。如果子问题之间有重叠,那就不适合用分治,而应该考虑动态规划。比如斐波那契数列,用递归分治写出来是指数级复杂度,因为大量子问题被重复计算。这时候要么加备忘录变成动态规划,要么直接用迭代。
第二个关键点是合并操作的代价。分治的时间复杂度等于子问题数量乘以子问题规模对应的复杂度,再加上合并的代价。归并排序之所以是O(n log n),是因为每次合并需要O(n)的时间,而递归深度是log n。如果合并操作太慢,分治的优势就被抵消了。
第三个关键点是递归基的选择。递归不能无限进行下去,必须有一个终止条件。这个终止条件选得好不好,直接影响效率。比如在快速排序中,当子数组长度小于某个阈值时,改用插入排序,实际运行速度会比纯递归快不少。这是因为小数组上递归调用的开销超过了算法本身的收益。
以归并排序为例,完整的分治过程是这样的:
def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result这段代码的时间复杂度分析:每次递归把数组分成两半,所以递归深度是log n。每一层的合并操作总共处理n个元素,所以每层的时间是O(n)。总复杂度就是O(n log n)。空间复杂度是O(n),因为合并时需要额外的数组来暂存结果。
分治法在实际工程中应用非常广。比如大数据处理里的MapReduce模型,本质上就是分治思想:把大任务拆成小任务分发到多台机器上并行处理,最后汇总结果。再比如计算几何里的最近点对问题,暴力解法是O(n²),用分治可以降到O(n log n)。这些案例都说明,分治不是纸上谈兵,而是能实实在在提升性能的武器。
2.3 动态规划:用空间换时间的艺术
动态规划是算法课里最难啃的骨头之一,但也是最有价值的部分。它的核心思想是把重叠子问题的解存起来,避免重复计算。和分治最大的区别在于,分治的子问题相互独立,动态规划的子问题相互重叠。
理解动态规划,关键要抓住三个要素:状态定义、状态转移方程、边界条件。这三个东西搞清楚了,代码就是水到渠成的事。
状态定义是第一步,也是最难的一步。所谓状态,就是用一个或多个变量来描述问题在某个阶段的状况。比如背包问题,状态可以定义为“前i个物品在容量为j的背包里能装的最大价值”。这个定义要满足两个条件:一是能覆盖所有情况,二是能通过子状态推导出来。
状态转移方程描述的是状态之间怎么过渡。还是背包问题,对于第i个物品,有两种选择:放进去或者不放。如果不放,最大价值等于前i-1个物品在容量j下的最大价值;如果放,最大价值等于前i-1个物品在容量j减去第i个物品重量下的最大价值,再加上第i个物品的价值。取两者的较大值,就是转移方程。
边界条件就是最简单的情况,通常是i等于0或者j等于0的时候,价值为0。
以最长公共子序列为例,状态转移方程是这样的:
def lcs(s1, s2): m, n = len(s1), len(s2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[m][n]这个算法的时间复杂度是O(mn),空间复杂度也是O(mn)。如果只需要求长度而不需要还原具体序列,空间可以优化到O(min(m, n)),因为每次计算只依赖上一行和当前行。
动态规划的实操心得里,有一条特别重要:先写递归版本,再加备忘录,最后改成迭代。递归版本最直观,容易验证正确性;加备忘录就是把重复计算的结果存起来;改成迭代是为了避免递归栈溢出,同时提升常数级性能。这个三步走的方法,能帮你绕过很多坑。
提示:动态规划的题目千变万化,但万变不离其宗。拿到题目先问自己三个问题:状态是什么?转移方程是什么?边界在哪里?这三个问题答不上来,说明还没想清楚,不要急着写代码。
2.4 贪心算法:每一步都选当前最好的
贪心算法的思路非常符合直觉:每一步都做出当前看起来最优的选择,希望最终得到全局最优解。但这里有个致命的问题:局部最优不等于全局最优。所以贪心算法的难点不在于怎么写,而在于怎么证明它是对的。
什么时候可以用贪心?需要满足两个性质:贪心选择性质和最优子结构性质。贪心选择性质说的是,每一步的局部最优选择能导致全局最优解。最优子结构说的是,问题的最优解包含其子问题的最优解。这两个性质必须同时满足,缺一不可。
经典的贪心算法例子包括:活动选择问题、霍夫曼编码、最小生成树的Prim和Kruskal算法、单源最短路径的Dijkstra算法。这些算法的共同特点是,每一步的选择都是确定的,不需要回头修改。
以活动选择问题为例,给定一组活动,每个活动有开始时间和结束时间,要求选出最多的互不冲突的活动。贪心策略是:每次选择结束时间最早且与已选活动不冲突的活动。这个策略为什么对?因为结束时间越早,留给后面活动的时间就越多。
def activity_selection(activities): activities.sort(key=lambda x: x[1]) selected = [activities[0]] last_end = activities[0][1] for start, end in activities[1:]: if start >= last_end: selected.append((start, end)) last_end = end return selected贪心算法的时间复杂度通常很低,因为只需要排序加一次遍历。上面这个例子就是O(n log n),主要花在排序上。
但贪心不是万能的。比如找零钱问题,如果硬币面额是1、5、11,要凑15块钱,贪心会先拿11,再拿四个1,总共五个硬币。但最优解是三个5,只要三个硬币。所以贪心策略依赖于具体的面额设置,不能盲目套用。
我的经验是,能用动态规划就用动态规划,贪心只在能严格证明的情况下使用。因为动态规划虽然复杂度高一些,但正确性有保障。贪心一旦策略选错,结果就是错的,而且不容易发现。
3. 从理论到代码的完整实操过程
3.1 环境准备与工具选择
学算法不需要太复杂的开发环境,但选对工具能省不少事。我的建议是准备两套环境:一套用于日常练习和调试,一套用于性能测试和对比。
日常练习用Python就够了,语法简洁,写伪代码和实际代码之间的转换成本最低。IDE用VS Code加Python插件,或者PyCharm社区版都行。关键是配置好调试功能,能单步执行、查看变量、设置断点。算法调试和普通业务代码调试不一样,你需要观察递归调用栈、数组状态变化、循环变量取值,这些用调试器看比打印日志高效得多。
性能测试建议用C++或者Java,因为这两种语言能更精确地控制内存和CPU,测出来的时间更接近理论分析。C++用g++编译,加-O2优化,用chrono库计时。Java用JMH做基准测试,避免JIT预热带来的误差。
在线判题平台也是必备的。LeetCode、洛谷、Codeforces这些平台提供了大量算法题目和测试用例,能帮你验证代码的正确性和效率。我习惯在本地写完代码后,再到平台上跑一遍,看看有没有边界情况没考虑到。
注意:不要一上来就追求最优解。先用最直观的方法把问题解出来,确保正确性,然后再逐步优化。很多初学者卡在“想不出最优解”上,结果连暴力解法都没写出来。暴力解法虽然慢,但它是验证最优解的基准,也是理解问题本质的起点。
3.2 以最大子数组和为例的完整实现
最大子数组和问题:给定一个整数数组,找到一个连续子数组,使得其元素之和最大。这个问题有四种解法,复杂度从O(n³)到O(n),非常适合用来演示算法优化的过程。
暴力解法一:枚举所有子数组的起点和终点,然后求和。三重循环,时间复杂度O(n³)。
def max_subarray_brute_force(arr): n = len(arr) max_sum = float('-inf') for i in range(n): for j in range(i, n): current_sum = sum(arr[i:j+1]) max_sum = max(max_sum, current_sum) return max_sum暴力解法二:优化求和过程,用前缀和或者累加的方式,把求和降到O(1)。两重循环,时间复杂度O(n²)。
def max_subarray_quadratic(arr): n = len(arr) max_sum = float('-inf') for i in range(n): current_sum = 0 for j in range(i, n): current_sum += arr[j] max_sum = max(max_sum, current_sum) return max_sum分治解法:把数组分成左右两半,最大子数组要么在左半边,要么在右半边,要么跨越中点。递归求解左右两边,再计算跨越中点的最大和,取三者最大值。时间复杂度O(n log n)。
def max_subarray_divide_conquer(arr, left, right): if left == right: return arr[left] mid = (left + right) // 2 left_max = max_subarray_divide_conquer(arr, left, mid) right_max = max_subarray_divide_conquer(arr, mid + 1, right) cross_max = max_crossing_sum(arr, left, mid, right) return max(left_max, right_max, cross_max) def max_crossing_sum(arr, left, mid, right): left_sum = float('-inf') current = 0 for i in range(mid, left - 1, -1): current += arr[i] left_sum = max(left_sum, current) right_sum = float('-inf') current = 0 for i in range(mid + 1, right + 1): current += arr[i] right_sum = max(right_sum, current) return left_sum + right_sum动态规划解法:定义dp[i]为以第i个元素结尾的最大子数组和。转移方程是dp[i] = max(arr[i], dp[i-1] + arr[i])。时间复杂度O(n),空间复杂度O(n),可以优化到O(1)。
def max_subarray_dp(arr): n = len(arr) dp = [0] * n dp[0] = arr[0] max_sum = dp[0] for i in range(1, n): dp[i] = max(arr[i], dp[i-1] + arr[i]) max_sum = max(max_sum, dp[i]) return max_sum def max_subarray_optimized(arr): max_sum = current_sum = arr[0] for num in arr[1:]: current_sum = max(num, current_sum + num) max_sum = max(max_sum, current_sum) return max_sum这四种解法的对比非常能说明问题。当数组长度是一万的时候,O(n³)的解法要跑几十分钟,O(n²)要跑几秒,O(n log n)和O(n)都是毫秒级。这就是算法优化的威力。
3.3 性能测试与对比分析
光看理论复杂度还不够,实际跑一下才能感受到差距。我写了一个简单的测试脚本,用不同规模的随机数组来对比四种解法的运行时间。
import random import time def benchmark(func, arr, name): start = time.perf_counter() result = func(arr) end = time.perf_counter() print(f"{name}: result={result}, time={end-start:.6f}s") sizes = [100, 500, 1000, 2000] for size in sizes: arr = [random.randint(-100, 100) for _ in range(size)] print(f"\nArray size: {size}") benchmark(max_subarray_brute_force, arr, "Brute Force O(n^3)") benchmark(max_subarray_quadratic, arr, "Quadratic O(n^2)") benchmark(lambda a: max_subarray_divide_conquer(a, 0, len(a)-1), arr, "Divide Conquer O(n log n)") benchmark(max_subarray_optimized, arr, "DP O(n)")实测结果很能说明问题。数组长度100的时候,四种解法差距还不算太夸张,暴力解法大概几毫秒,优化解法微秒级。但长度到2000的时候,暴力解法已经要好几秒了,而O(n)解法还是微秒级。这个差距随着规模增大会继续拉大。
这里有个细节值得注意:分治解法虽然理论复杂度是O(n log n),但在小数组上实际运行时间可能比O(n²)还慢,因为递归调用有额外开销。所以实际工程中,经常会在递归到小规模时切换到暴力解法,这就是所谓的“混合策略”。
提示:性能测试的时候要注意JIT预热和缓存效应。Python的解释器会缓存一些中间结果,第一次运行和后续运行的时间可能不一样。建议每个测试跑多次取平均值,并且把数组随机化,避免缓存带来的偏差。
4. 常见问题与排查技巧实录
4.1 复杂度分析中的典型错误
错误一:把循环嵌套层数直接等同于复杂度指数。两层循环不一定是O(n²),如果内层循环的边界依赖于外层变量,实际执行次数可能远小于n²。比如前面提到的for j in range(i, n),实际是n(n+1)/2,虽然还是O(n²),但常数因子是1/2。更极端的情况,如果内层循环每次只执行常数次,那整体就是O(n)。
错误二:忽略递归调用的复杂度。递归函数的时间复杂度等于递归次数乘以每次递归内部的操作数。比如二分查找,递归深度是log n,每次递归内部做常数次比较,所以总复杂度是O(log n)。但如果是归并排序,每次递归内部要做O(n)的合并,所以总复杂度是O(n log n)。
错误三:混淆平均复杂度和最坏复杂度。快速排序的平均复杂度是O(n log n),但最坏情况是O(n²)。如果面试官问“快速排序的复杂度是多少”,你应该回答“平均O(n log n),最坏O(n²)”。只说一个都是不完整的。
错误四:忘记考虑空间复杂度。有些算法时间上很快,但空间开销大。比如计数排序,时间复杂度O(n+k),但需要O(k)的额外空间。如果k远大于n,这个空间开销就不可接受了。
4.2 动态规划的状态定义陷阱
动态规划最难的地方在于状态定义。状态定义错了,转移方程就写不出来,或者写出来是错的。我总结了几种常见的状态定义陷阱。
陷阱一:状态维度不够。有些问题需要两个甚至三个维度来描述状态。比如背包问题,如果只定义“前i个物品的最大价值”,就无法区分不同容量下的情况。必须加上容量维度,变成“前i个物品在容量j下的最大价值”。
陷阱二:状态之间有后效性。所谓后效性,就是当前状态的选择会影响后续状态的转移。比如最长递增子序列问题,如果定义dp[i]为“前i个元素的最长递增子序列长度”,这个定义就有后效性,因为不知道子序列的最后一个元素是什么,无法判断下一个元素能不能接上去。正确的定义是dp[i]为“以第i个元素结尾的最长递增子序列长度”。
陷阱三:边界条件处理不当。动态规划的边界条件往往对应着最简单的情况,但也是最容易出错的地方。比如背包问题中,容量为0时最大价值为0,这个边界条件如果漏掉,整个表就填不对。
注意:写动态规划的时候,建议先把dp表画出来,手动填几行,看看转移方程对不对。这个步骤看起来笨,但能帮你发现很多逻辑错误。我见过太多人直接写代码,结果调试半天发现是状态定义错了。
4.3 贪心算法的正确性验证
贪心算法最大的风险是策略选错。验证贪心策略是否正确,有两种方法:交换论证法和归纳法。
交换论证法的思路是:假设存在一个最优解,它和贪心解在某个步骤做出了不同的选择。然后证明通过交换选择,可以得到一个不差于原最优解的新解。这样一步步交换下去,最终贪心解不会比最优解差,所以贪心解就是最优解。
归纳法的思路是:证明贪心算法在前i步都能保持最优性,然后证明第i+1步也能保持。这个方法适合步骤之间有明确递推关系的问题。
以活动选择问题为例,交换论证是这样的:假设最优解选择的第一个活动不是结束时间最早的,那么我们可以把它替换成结束时间最早的那个活动,这样不会减少后续可选活动的数量,所以替换后的解仍然是最优的。以此类推,贪心解就是最优解。
如果实在证明不了贪心策略的正确性,那就不要用贪心。改用动态规划或者暴力搜索,虽然复杂度高一些,但至少结果是对的。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 程序运行超时 | 算法复杂度过高 | 分析代码的循环嵌套和递归深度 | 优化算法,降低复杂度 |
| 结果不正确 | 边界条件未处理 | 检查输入为空、长度为1等特殊情况 | 补充边界条件判断 |
| 递归栈溢出 | 递归深度过大 | 打印递归深度或改用迭代 | 加备忘录或改成迭代版本 |
| 动态规划结果错误 | 状态定义有后效性 | 检查状态是否包含足够信息 | 重新定义状态,增加维度 |
| 贪心结果不是最优 | 贪心策略不满足贪心选择性质 | 构造反例验证 | 改用动态规划 |
| 内存超出限制 | 空间复杂度过高 | 分析辅助空间的使用 | 优化空间,使用滚动数组 |
这张表是我在实际做题和项目中总结出来的,基本上覆盖了八成以上的常见问题。遇到问题的时候,先对照这张表排查,能省不少时间。
4.5 独家避坑技巧
技巧一:先写测试用例再写代码。拿到一个问题,先想几个简单的测试用例,包括正常情况、边界情况、极端情况。然后写代码的时候,心里有这些用例,就不容易漏掉边界条件。
技巧二:用打印大法调试递归。递归代码出问题的时候,在函数入口打印参数,在函数出口打印返回值。这样能清楚地看到递归调用树,快速定位问题。
技巧三:动态规划先写递归版。递归版动态规划最直观,容易验证正确性。写完之后加备忘录,再改成迭代。这个三步走的方法能帮你绕过很多坑。
技巧四:复杂度分析先看最坏情况。实际工程中,最坏情况决定了系统的稳定性。如果最坏情况不可接受,就要考虑换算法或者加限制条件。
技巧五:多画图少空想。算法问题很多时候画个图就清楚了。递归树、状态转移图、流程图,这些可视化工具能帮你理清思路,比在脑子里空想高效得多。
5. 算法思维在实际项目中的落地方式
5.1 从算法课到工程代码的距离
算法课上的伪代码和实际工程代码之间,隔着不少东西。伪代码假设输入是完美的,不考虑异常处理、内存管理、并发安全。实际工程中,这些恰恰是最耗精力的部分。
举个例子,算法课上的快速排序是这样的:选一个基准,把小于基准的放左边,大于基准的放右边,递归处理左右两边。但实际工程中,你需要考虑:输入数组可能为空、可能已经有序、可能包含大量重复元素、可能数据量太大导致递归栈溢出。这些问题在算法课上通常不讨论,但在工程中必须处理。
所以我的建议是:算法课上学的是思路,工程中用的是实现。思路对了,实现可以慢慢打磨。但思路错了,实现再精致也没用。
5.2 算法优化在真实场景中的收益
算法优化带来的性能提升,在真实场景中往往比想象的大。我参与过一个数据处理项目,原始代码用嵌套循环做数据匹配,数据量到十万条的时候要跑十几分钟。后来改成哈希表匹配,同样的数据量跑不到一秒。这个提升不是百分之几十,是上千倍。
另一个例子是推荐系统里的相似度计算。原始实现用双重循环计算所有物品对之间的相似度,复杂度O(n²)。后来改成基于倒排索引的近似计算,复杂度降到O(n log n),在千万级物品库上,计算时间从几小时降到几分钟。
这些案例说明,算法优化不是学术游戏,而是能实实在在节省时间和资源的工程手段。尤其是在数据量大的场景下,算法复杂度的差异会被放大到不可忽视的程度。
5.3 持续练习的方法与资源
算法能力的提升没有捷径,就是持续练习。但练习也要讲方法,盲目刷题效率很低。
我的方法是按专题刷,不按难度刷。先集中刷一类问题,比如动态规划,把各种变体都做一遍,总结出通用的解题模板。然后再换下一个专题。这样比随机刷题效率高得多,因为同一类问题的思路是相通的,集中练习能帮你快速建立模式识别能力。
刷题的时候,不要只看答案。看完答案之后,合上答案自己写一遍。写完之后,再想想有没有其他解法,能不能优化。这个过程比单纯看答案慢,但效果好得多。
推荐几个练习资源:LeetCode适合面试准备,题目分类清晰,讨论区质量高;洛谷适合算法竞赛入门,题目难度梯度合理;Codeforces适合进阶,题目质量高但难度也大。选一个适合自己的平台,坚持刷下去就行。
提示:刷题不在多而在精。与其刷一千道题但每道都一知半解,不如刷两百道题但每道都吃透。吃透的标准是:能独立写出代码、能分析复杂度、能解释为什么这样做、能举一反三。
5.4 算法面试中的常见考察点
算法面试考察的不仅仅是你能不能写出代码,更重要的是你的思考过程。面试官想看的是:你拿到问题后怎么分析、怎么选择算法、怎么处理边界情况、怎么优化。
常见的考察点包括:复杂度分析能力、算法选择能力、代码实现能力、边界处理能力、沟通表达能力。其中沟通表达能力往往被忽视,但实际上很重要。你需要把思路清晰地讲出来,让面试官跟上你的节奏。
面试中的一个小技巧:先讲思路再写代码。拿到问题后,先跟面试官确认理解是否正确,然后讲你的思路和复杂度分析,得到认可后再动手写。这样即使代码有小问题,面试官也能看到你的思考过程。
另一个技巧是主动处理边界情况。写完代码后,主动说“这里我处理了空输入的情况”“这里我考虑了数组长度为1的情况”。这能体现你的严谨性,给面试官留下好印象。
算法设计与分析这门课,学的时候可能觉得抽象,但用起来是真的香。它给你的不是几个具体的算法,而是一套分析问题、解决问题的思维框架。这套框架一旦建立起来,你看待代码的方式会完全不同。以前你可能只关心“能不能跑”,现在你会关心“跑得快不快”“有没有更好的办法”。这个转变,才是这门课最大的价值。