我第一份工作入职第一周,导师让我写一个冒泡排序。我当时觉得这还不简单,两分钟交上去,结果他看了一眼就问:“这个排序稳定吗?最好情况和最坏情况时间复杂度差多少?能不能优化?”我当场愣住。后来我才发现,算法这行真正难的从来不是“写出来”,而是“想明白”。很多新手容易栽跟头,不是因为代码量不够,而是对最基础的概念、最常见的坑没有建立起系统的认知。
这篇文章就是写给正在学算法的新手朋友。我会结合实际场景,把复杂度、排序、二分查找、递归、动态规划这些绕不开的简单算法拆开讲清楚,同时把新手最容易踩的坑和排查思路一并整理出来。不管你是科班在读还是转行自学,只要能把这篇内容吃透,再回头刷题或做项目,会觉得顺很多。整篇内容我已经过滤掉了晦涩的数学推导,尽可能用实际案例和代码说话,你可以直接照着拿到自己的代码里跑一遍。
1. 算法学习,先搞懂这5个底层概念
很多人一上来就刷 LeetCode,觉得“题刷得越多越厉害”。我不反对刷题,但你得先搞懂几个底层概念,否则刷题只是在背答案,换个场景就不会了。我个人认为,新手阶段最值得先花时间的是下面这5个点:时间复杂度、数据结构选型、递归思想、空间换时间、调试意识。
1.1 时间复杂度:不要把 O(n) 和“运行时长”划等号
时间复杂度,准确说是“渐近时间复杂度”,它描述的是算法执行时间随数据规模增长的变化趋势,而不是具体跑多少毫秒。举个实际例子,你的机器跑一个 O(n) 的算法,在 n=1000 时可能只要0.01秒;但一个 O(n²) 的算法在 n=1000 时可能要1秒;看起来差距不大,可当 n 变成 100000 时,O(n) 还是很快,而 O(n²) 就会跑到天荒地老。
理解这个概念,你需要掌握几个常见复杂度量级,从快到慢大概是:O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)。很多新手会忽略常数项,比如 O(2n) 和 O(100n) 本质上都是 O(n),但在实际使用中常数项也值得关注。面试或工程里,我们通常会关注最坏情况复杂度,同时也要留意平均情况,比如快速排序平均 O(n log n),但最坏 O(n²),这点后面细说。
1.2 数据结构:算法和容器是绑定的
学算法不能脱离数据结构。你要排序,得知道数组和链表哪个更适合随机访问;要频繁在头部插入删除,那链表比数组好使;要实现撤销操作,栈是天然选择;要实现排队场景,队列更合适;要快速判断一个元素是否存在,哈希表的时间复杂度是 O(1)。
我见过不少新手,链表还没搞明白就去啃图论,结果写出来的“图遍历”实际上是在数组里瞎转。我给的建议是:先把数组、链表、栈、队列、哈希表、树这六种基础结构的基本操作和适用场景吃透,再开始学排序和搜索。数据结构是容器的选型,算法的本质是在容器上做的操作策略,两者无法分开谈。
1.3 递归思想:先学会“相信函数”
递归是新手最头疼的点之一。我总结了一个方法:你要学会“相信这个函数能完成它的功能”。比如你要写一个计算阶乘的函数fact(n),你只需要明确两点:第一,n=0或n=1时返回1,这是递归出口;第二,对于任意n>1,fact(n) = n * fact(n-1),这里假设fact(n-1)已经能正确工作。
很多新手写递归时会不由自主地在脑子里展开全部调用链,试图“人肉模拟”每一层调用。这在递归深度很浅时还可以,一旦超过四五层就晕了。正确的做法是“宏观审视”,只关心当前这一层做什么、递归出口在哪里、参数如何向出口收敛。只要这三点想清楚,递归基本不会写错。当然,递归也并非没有代价,每一层调用都会产生函数栈帧,占用内存,深度太大还会栈溢出,这个我们后面详细展开。
1.4 空间换时间:算法的精髓之一
很多算法优化的核心思路就是“用空间换时间”。典型代表是哈希表和动态规划。哈希表用额外内存存储键值映射,换取 O(1) 的查询时间;动态规划用数组记录子问题的解,避免重复计算。新手在学算法时往往不舍得开数组,或者不知道怎么用缓存,导致写的代码逻辑对但性能差。
我建议你在分析任何算法的时候,都养成一个习惯:问自己“有没有什么东西被我重复算了很多次?能不能存下来复用?”这个问题一旦开始出现在你的脑子里,你对算法的理解就上了一个台阶。后文讲斐波那契数列时,你会看到同一个问题在“重复计算”和“记忆化”之间的巨大差异,那就是“空间换时间”最生动的演示。
1.5 调试与测试意识:新手最容易忽略的
很多新手写完代码能跑通就觉得自己“会了”,但算法题的坑往往藏在边界条件里。比如循环里数组越界、空数组、单元素数组、全相等数组、已经有序的数组……这些用例在实际工作中远比“标准样例”更容易暴露问题。我自己的习惯是:每次写完一个算法,先用最极端的几个例子测一下,再跑正常数据。不要嫌麻烦,这在找工作时就是你能不能通过面试的关键。
调试工具方面,IDE 的断点调试是必须掌握的,但很多人更喜欢用 print 大法,这也没问题。关键是要有“定位意识”:出问题时,不要满屏幕加 print,而是先根据报错信息判断是逻辑问题还是内存问题,再用二分法缩小排查范围。比如一个排序算法结果不对,就先打印中间状态,看第几次交换后顺序开始乱,这样很容易定位到具体的比较或交换逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 新手必学的三大排序算法
排序算法是算法学习的“必修课”,也是面试常客。这部分只挑三个对新手最有价值的来讲:冒泡排序、选择排序、快速排序。归并排序也很重要,但它在理解了分治思想之后更容易上手,所以放到快速排序里一起说。
2.1 冒泡排序:理解“比较”和“交换”的入门砖
冒泡排序的思路很直白:从头到尾相邻两个元素两两比较,如果前一个比后一个大,就交换它们。这样每一轮下来,最大的元素就像气泡一样“冒”到数组末尾。重复 n-1 轮后,数组就有序了。
python复制def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
这段代码里有个细节:内层循环的终止条件是 n - 1 - i,因为每一轮结束后,末尾 i 个元素已经是排好序的,不需要再比较。这个细节能省掉不少无意义的比较,虽然不改变复杂度阶数,但确实能减少常数时间。
冒泡排序的时间复杂度:最坏和平均都是 O(n²),最好情况(数组已经有序)如果加上优化,可以做到 O(n)。优化方法很简单:设置一个 swapped 标志位,如果某一轮没有任何交换发生,说明数组已经有序,提前结束。
python复制def bubble_sort_optimized(arr):
n = len(arr)
for i in range(n - 1):
swapped = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
冒泡排序的稳定性是它一个很重要的特性。所谓稳定,指的是两个相等元素在排序前后的相对位置不变。有些场景(如按多个字段排序时)稳定性很关键。我面试时经常问候选人“冒泡排序稳定吗”,能答上来的不多。答案是稳定,因为 arr[j] > arr[j + 1] 使用的严格大于,相等元素不会交换位置。
2.2 选择排序:找最小值的思路比排序本身更重要
选择排序的思路是:每一轮从未排序区间中找到最小值,放到已排序区间的末尾。和冒泡排序的“两两交换”不同,选择排序每一轮最多只交换一次。
python复制def selection_sort(arr):
n = len(arr)
for i in range(n - 1):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
选择排序无论数据初始状态如何,比较次数都是固定的 n(n-1)/2,所以最好、最坏、平均时间复杂度都是 O(n²)。它的优点是比较次数稳定,交换次数少,但它的稳定性比较差。比如 arr = [5, 8, 5, 2],第一轮找到最小元素 2,把它和第一个 5 交换,两个 5 的相对顺序就被打乱了。所以选选择排序时要先想清楚是否要求稳定性。
说实话,工程里很少有人直接用冒泡或选择排序,因为 O(n²) 对大数据量来说太慢。但作为教学入门,它们是理解“比较-交换”范式的基石。前几年有个很有意思的事,很多面试官喜欢拿“冒泡排序优化”当热身题,核心就是那个 swapped 标志位。你掌握了这个,至少能让面试官觉得你有工程思维。
2.3 快速排序:分治思想的启蒙课
快排是面试和工程里的常客,它基于分治思想:选一个基准值,把数组分成小于基准和大于基准的两部分,然后递归地对两部分排序。整个过程就像把一个班的学生按分数分成两组,再在组内继续分,直到每个组只剩一个人。
python复制def quick_sort(arr, left, right):
if left >= right:
return
pivot = partition(arr, left, right)
quick_sort(arr, left, pivot - 1)
quick_sort(arr, pivot + 1, right)
def partition(arr, left, right):
pivot = arr[right]
i = left
for j in range(left, right):
if arr[j] < pivot:
arr[i], arr[j] = arr[j], arr[i]
i += 1
arr[i], arr[right] = arr[right], arr[i]
return i
这个实现里 partition 是快排的灵魂。它用双指针把数组原地分割,最后把基准值放到正确位置并返回其下标。平均情况下,每次 partition 都将数组近似对半分割,递归深度是 O(log n),每一层处理的总元素数是 O(n),所以平均时间 O(n log n)。但最坏情况下,如果每次基准都恰好是最大或最小值,分割严重不平衡,递归深度会变成 O(n),总时间退化为 O(n²)。
避免最坏情况的一个技巧是“三数取中”:从数组头、中、尾三个位置取中间值作为基准,这样能大幅降低最坏情况的概率。另外一个心得是:当子数组规模小于某个阈值(比如 10)时,改用插入排序,避免递归开销。这个优化在工程中很常见,很多标准库的排序实现都这么干。
归并排序和快排正好相反:快排先处理再递归,归并先递归再合并。归并排序稳定,时间稳定在 O(n log n),但需要额外 O(n) 空间。你可以把归并理解为“先把数组拆成另一半,再像合并两副有序扑克牌一样合并”。理解了快排的分治思想后再看归并,会轻松很多。
2.4 排序算法怎么选:一张表看懂
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
实际工程里,如果你确定数据量小且基本有序,插入排序反而很快。如果数据量很大且要求稳定,归并排序更合适。如果空间有限,堆排序值得考虑。如果只想写起来顺手且不关心稳定性,快排是默认选项。记住,没有银弹,只有适不适合当前场景。
3. 从线性搜索到二分查找:边界和细节决定成败
搜索是算法里和排序平级的重点。线性搜索最容易理解,就是从头到尾挨个找,时间复杂度 O(n),适合无序小数据。真正有价值的是二分查找,它能在有序数组中用 O(log n) 的时间找到目标。但二分查找看似简单,实际写对的人并不多,边界条件是个大坑。
3.1 二分查找:一个模板解决所有边界问题
二分查找的核心思想是“每次砍掉一半”。假设你有一个升序数组,取中间位置元素和目标比较:如果相等,返回下标;如果中间值小于目标,说明目标在右半部分;如果大于,则在左半部分。每次将搜索区间缩小一半。
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
这个模板的关键是 while left <= right。选 <= 还是 <,取决于你对区间定义的理解。我习惯使用闭区间 [left, right],所以循环条件是 left <= right,当 left == right 时,区间里还有一个元素需要判断,不能退出。如果使用左闭右开 [left, right),循环条件就变成 while left < right,right 的初始值变为 len(nums)。两种写法都可以,但你必须保持区间定义和循环条件一致,否则就会出错。
还有一个细节是 mid = (left + right) // 2。当 left 和 right 都很大时,直接相加可能溢出(Python 不担心,但 C++ 或 Java 要小心),所以更安全的写法是 mid = left + (right - left) // 2。这个写法在很多大公司面试里是考察点之一。
3.2 二分查找的常见错误与“查找左边界”变体
新手常见的错误包括:忘记 mid + 1 或 mid - 1,导致死循环;循环条件写错导致漏查或重复查;计算 mid 时用 right = mid 而不是 right = mid - 1,在特定区间定义下会死循环。
比基础二分查找更难一点的是“查找第一个等于目标的元素”或“最后一个小于等于目标的元素”。这类变体在算法题里非常常见,比如“寻找旋转排序数组中的最小值”“搜索插入位置”。核心思路不变,但在 nums[mid] == target 时不立即返回,而是继续压缩右边界,直到找到最左端的匹配项。
我自己在实际工作中,经常要用二分查找处理时间序列数据。比如一个按时间排序的日志数组,要快速找到某个时间点之前最近的一条记录,就是标准的二分查找右边界变体。掌握模板和边界推导,能让你在这些场景中直接拿来即用。
3.3 从二分到图搜索:Dijkstra 和 A* 的通俗理解
二分查找解决的是“一维有序空间”的搜索,但很多实际问题是在图里搜索最短路径。热词里提到的迪杰斯特拉算法(Dijkstra)、A* 算法、AGV 路径规划,都属于这个范畴。作为新手,先不用把图算法啃得很深,但理解它们和搜索之间的关系,对后续学习很有帮助。
Dijkstra 算法的思路可以这样理解:你站在一个陌生城市的十字路口,想去一个目的地。你不知道整体路网结构,但你每到一个路口,就能知道从当前路口走到相邻路口需要多长时间。你每次都优先走“当前已知最短路径”的那个方向。本质上,它维护一个“已确定最短路径”的集合,每次从未确定集合中选距离最小的节点,用这个节点去松弛它的邻居。
Dijkstra 不能处理负权边,这是个经典考点。原因是:算法假设一旦节点被选中(距离确定),之后就不会再被更短的路径更新。但如果有负权边,已经确定的节点可能被后面的负权路径更新成更短的距离,这个假设就崩了。所以如果有负权边,要改用 Bellman-Ford 或 SPFA。
A* 可以理解为 Dijkstra 的升级版。Dijkstra 只考虑“从起点到当前点的实际代价”,A* 额外加了一个“从当前点到终点的估计代价”(启发式函数),总代价 = 实际代价 + 估计代价。这就像导航软件不仅知道你已经开了多远,还能大致估计到终点还有多远,从而优先走“看起来更可能到达目标”的方向。
AGV 小车在仓库里找路径时,A* 非常常用,因为它比 Dijkstra 更快、更有方向性。热词里还提到了“基于改进冲突搜索的多机器人路径规划”,那是多智能体路径规划领域了,核心思想是在 A* 的基础上为每台车规划路径,再检测冲突并迭代调整。新手如果能把单机的 A* 吃透,再看那些论文会轻松不少。
4. 递归进阶:从斐波那契到动态规划
递归和动态规划是算法里的“分水岭”,很多新手止步于此。我的经验是,不要急着去背状态转移方程,先把最经典的几个问题用“暴力递归 → 记忆化 → 动态规划”的路径走一遍,才能真正理解它。
4.1 斐波那契数列:同一个问题,三种写法差距巨大
斐波那契数列的定义很简单:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)。新手会很自然地写出递归版本:
python复制def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
这段代码逻辑没错,但性能极差。它的递归树是一棵近似满二叉树,时间复杂度是 O(2ⁿ)。n=40 时,我的机器要跑 20 多秒;n=50 几乎跑不完。核心问题是大量重复计算,比如 fib(4) 会被算很多很多次。
优化思路就是“空间换时间”。加一个字典或数组,把已经算过的值存下来:
python复制def fib_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
这样每个子问题只算一次,时间复杂度降为 O(n),空间 O(n)。进一步优化,可以用滚动变量把空间压到 O(1):
python复制def fib_iter(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
| 写法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
| 纯递归 | O(2ⁿ) | O(n)(栈深度) | 好理解但极度低效 |
| 记忆化递归 | O(n) | O(n) | 方便从暴力递归升级 |
| 迭代滚动 | O(n) | O(1) | 最优性能,需理解循环关系 |
斐波那契是理解“动态规划就是把重复子问题的解存起来”的最佳例子。你如果能把这段从递归到记忆化再到迭代的演进过程亲手写一遍,比背十个算法题都有用。
4.2 爬楼梯问题:最经典的动态规划入门题
题目是这样的:你正在爬楼梯,每次可以爬 1 级或 2 级,问爬到 n 级有多少种不同方法。这个问题的递推思路是:要到达第 n 级,最后一步要么从第 n-1 级跨 1 级,要么从第 n-2 级跨 2 级,所以 dp[n] = dp[n-1] + dp[n-2],边界是 dp[0]=1(不动也是一种方式),dp[1]=1。
python复制def climb_stairs(n):
if n <= 1:
return 1
a, b = 1, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
你看,这个代码和斐波那契的迭代写法几乎一样。这就是动态规划的特点:一旦你把状态定义和转移方程搞清楚,代码往往很简洁。难点从来不是写代码,而是“定义状态”和“找转移关系”。
我分享一下做动态规划题目的固定套路:第一步,定义 dp[i] 表示什么,通常是“达到当前状态时的最优解或方案数”;第二步,写出状态转移方程,即 dp[i] 和更小状态的关系;第三步,确定初始条件;第四步,确定遍历顺序。前两步是最难的,需要大量练习。爬楼梯是最简单的模板,后面你可以尝试“最小路径和”“最长递增子序列”来加深理解。
4.3 贪心、动态规划、暴力搜索怎么选
新手在拿到一个优化问题时,经常纠结该用哪种算法。我的建议是:先想暴力怎么做,再看有没有重复子问题,有就上动态规划或记忆化搜索;如果每一步决策都是局部最优且能推导出全局最优,再用贪心。贪心不是所有场景都适用,新手很容易误用。
比如“零钱兑换”问题,如果用贪心策略从最大面额开始找,在某些币值组合下得不到最优解。而动态规划可以保证全局最优。更直白地说,动态规划是“把问题空间系统地搜索一遍,同时记录中间结果”,贪心是“每一步选当下最好的,不回头”。贪心做不到动态规划那么稳,但速度快。
5. 新手常见错误与排查技巧实录
算法思路搞清楚了,代码却经常跑出各种奇怪的结果。这一节整理我在带新人和自己踩坑过程中反复遇到的几类问题,以及定位问题的方法论。
5.1 数组越界:不是所有的越界都会报错
数组越界在 Python 里通常表现为 IndexError,还算友好。但在 C/C++ 里,越界访问不一定立即报错,可能读到脏数据,甚至悄悄改坏内存,造成“幽灵般”的 bug。我见过一个真实案例:新人在循环里写 for (int i = 0; i <= n; i++),结果把数组最后一个元素之外的内存写入了一个随机值,程序后面在处理这个“脏数据”时彻底崩溃。
排查数组越界,我总结了一个教训:凡是出现 arr[i]、arr[j]、arr[i + 1] 这种下标访问,先确认 i 和 j 的取值范围是否严格在 [0, len(arr)-1] 内。特别是边界条件,比如快排的 partition、二分查找的 mid 更新,都要画区间图手动推演一遍。图形化思考能大幅降低这类错误。
5.2 死循环:程序跑不完了怎么办
死循环通常是因为循环变量的更新不满足“向终止条件收敛”的原则。比如二分查找里如果某个分支忘记更新 left 或 right,就会在同一个区间里反复横跳。还有一种隐蔽情况:更新了变量但方向错误,比如需要缩小 right 却写成 right = mid(在闭区间写法里),导致区间始终包含多个元素。
定位死循环最有效的方法:在循环体里加一个计数器,限制最大迭代次数,然后打印每次循环的 left、right、mid 值,观察哪个变量没有变化。用不了几次你就能发现是哪个分支出了问题。另外,递归里的死循环往往表现为“无限递归”,最终栈溢出,这个我们下面讲。
5.3 栈溢出:递归没写好一定会遇到
栈溢出 (Stack Overflow) 的本质是函数调用层数太深,每一层调用都会占用栈空间,操作系统分配给线程的栈空间是有限的。比如在 Windows 上默认是 1MB,Linux 通常是 8MB。如果递归深度达到几万层,就会把栈空间打满。
新人在写递归时最容易犯的错是:递归出口条件缺失或写错,导致函数无限调用自己。排查步骤很简单:第一步,检查递归出口是否存在;第二步,检查递归参数是否向出口收敛。比如 fact(n) 里调用 fact(n+1),那么 n 永远不会变小,必然栈溢出。
如果确实需要很深的递归(比如处理超长链表),可以考虑把递归改成显式栈的迭代写法。Python 里递归深度默认限制在 1000 左右,超过就报 RecursionError,可以在开头 sys.setrecursionlimit(10000) 调大,但这只是治标,不治本。
5.4 用 print 和断点定位问题的实战套路
遇到算法结果不对时,我习惯用“二分法排查”:先在代码中间位置加 print,看看前半段执行完的数据是否符合预期。如果前半段是对的,那问题在后半段;如果不对,就把排查范围缩到前半段。以此类推,很快就能定位到具体是哪个变量、哪一步操作出了问题。
例如你在实现跨算法时会用到状态数组,每个状态有多个维度。打印整个数组可能信息量太大,这时只打印关键移动或临界状态的变量值。再配合断点调试器,在 if 条件处设置断点,逐步检查每个变量,比盲猜高效得多。对 Python 来说,pdb 或 IDE 自带的断点调试就够用;C++ 我习惯用 gdb,但 IDE 里的图形化调试对新手更友好。
5.5 常见问题速查表:照着查能节省两小时
| 现象 | 常见原因 | 排查思路 |
|---|---|---|
| 程序报 IndexError | 数组越界 | 检查循环边界和下标计算,特别是 <= 和 < |
| 程序卡住不结束 | 死循环 | 打印循环变量,检查是否向终止条件收敛 |
| 递归深度报错 | 递归出口缺失或参数不收敛 | 检查出口条件,确认递归参数单调变化 |
| 排序结果部分正确 | 比较运算符方向写反 | 用单步调试或打印中间状态定位第一次错序位置 |
| 快排最坏情况很慢 | 基准值选得不好 | 用三数取中或随机选基准 |
| 结果有重复元素 | 二分查找边界变体处理不当 | 返回前继续压缩边界 |
| 动态规划结果偏大 | 状态定义或转移方程有误 | 用小规模数据手工推演一遍 dp 数组 |
这张表是我在实际带人过程中慢慢整理出来的,建议你收藏。出现问题先别急着搜报错信息,对照表里排查一遍,绝大部分问题能很快定位。
最后再分享一个我觉得对新手特别管用的小习惯:每次写完算法,不要急着运行,先在草稿纸上用人脑模拟一遍小数据。比如冒泡排序就模拟一个长度为 4 的数组,手动画出每一轮比较和交换的过程,再和你代码里实际跑出来的结果比对。这个方法看起来慢,但能帮你建立对算法过程的直觉,而且能发现很多代码逻辑上说不通的地方。我当年学算法时就是用这个方法,把快排、归并、Dijkstra 全都手推过几遍,后来面试时能在白板上边画边讲,靠的就是这些基本功。算法学习没有捷径,但一定有更聪明的路径,希望这篇文章能帮你少走一点弯路。
