第一篇博客,我选了“简单算法入门”这个主题。原因很直白:算法是编程的地基,也是很多初学者卡住的第一道坎。我想把这块硬骨头拆开、炖烂,写清楚“算法到底是什么”“为什么要学它”“学的时候会遇到哪些坑”,让看完的人能真的动手写几个基础算法,而不是只记住几个名词。这篇文章既是我自己的学习笔记整理成文,也希望能帮到那些刚写完“Hello World”、准备往深处走一步的朋友。
真正开始写之后我才发现,算法入门并不是上来就甩一堆代码。它更像是在训练一种“拆解问题的脑回路”——同样的需求,用不同的思路去解,效率可能差几十倍。这个认知,比记住任何特定算法的实现都重要。
1. 算法入门到底在学什么
1.1 算法不是背代码,是解决问题的思路
很多初学者对算法有个误解,觉得算法就是一堆晦涩难懂的代码,背下来就能应付笔试面试。实际根本不是这么回事。算法的本质是“解决问题的步骤描述”,同一道题,你可以用笨办法慢慢算,也可以用聪明的办法瞬间出结果,这些不同的步骤设计,就是不同的算法。
我习惯用一个生活化的类比来解释算法。你去超市买东西,结账时有两排收银台,一排没人排队,另一排排了十几个人。正常人都会选择没人的那排,这个“看一眼两边队伍长度,做出选择”的过程,就是一次算法决策。算法就是把这种日常决策的逻辑,翻译成计算机能理解、能执行的精确步骤。
学习算法,最重要的收获不是背下来多少种排序、多少种查找,而是养成一种思维习惯:拿到问题先想清楚约束条件是什么、数据规模有多大、对时间或空间的容忍度有多高,然后才去选择匹配的方案。这种思维方式会在你写业务代码、设计系统架构的时候潜移默化地起作用。
1.2 时间复杂度和空间复杂度:给算法量体温
算法入门第一个必须搞懂的概念,不是快速排序,也不是动态规划,而是复杂度分析。简单说,复杂度就是衡量算法“跑得快不快”“占地方多不多”的标尺。
时间复杂度通常用大O表示法来描述,它不关心具体跑了多少毫秒,而是关心“当数据量 n 变大时,操作次数跟着怎么涨”。比如线性查找的复杂度是 O(n),意思就是数据翻一倍,查找的耗时也大约翻一倍。二分查找是 O(log n),数据翻一倍,耗时只增加一次比较操作,这就是降维打击式的差距。
空间复杂度则关注算法额外占用了多少内存。有些算法跑得飞快但消耗大量内存换时间,有些算法节省内存但慢吞吞,实际开发中怎么选,取决于你的场景是“内存金贵”还是“响应速度优先”。
入门阶段不需要把复杂度分析学得特别数学化,但至少要做到:看一段代码能判断它是 O(1)、O(n) 还是 O(n²),知道数据量大的时候哪些算法会崩。这个基本功越早建立越好,否则后面学数据结构、刷题库的时候会一直处于“看不懂优化思路”的懵圈状态。
2. 从查找开始:两个最友好的入门算法
2.1 线性查找:最直觉的做法
线性查找是最朴素的查找方案:从头到尾挨个看一遍,找到目标值就返回位置,全部看完都没有就返回“没找到”。它的代码实现简单到不需要任何技巧:
def linear_search(arr, target): for i in range(len(arr)): if arr[i] == target: return i return -1这个算法的思想就是“遍历+判断”,跟你在书架上找一本特定书的方式一模一样:从第一本开始翻,不是就下一本,直到找到为止。它的优点是简单可靠,数据无需任何预处理,数组是无序的也能用;缺点是效率低,数据量一大就不够看。
为什么先学这种看起来“笨”的算法?因为它是理解一切更优方案的原点。所有更快的查找算法,本质上都是在回答一个问题:我能不能少看几本书就找到目标?带着这个问题,你才会理解“为什么要排序”“为什么要建索引”这些后续概念的动机。
我实测过的经验是,线性查找在数据量小于一万的规模下完全没有性能压力。但一旦规模上到百万、千万,一次完整遍历就可能带来肉眼可见的卡顿。这就是为什么我们需要更聪明的查找策略。
2.2 二分查找:学会利用有序
二分查找的前提条件很明确:数据必须是有序的。它每次取中间位置的元素跟目标值比较,根据大小关系直接砍掉一半的查找范围,重复这个动作直到找到或者范围为空。
def binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1这个算法之所以经典,是因为它第一次让初学者直观感受到“数学优势”落在实处。在100万个有序数据里做线性查找,平均需要50万次比较;用二分查找,最多只需要20次。这个差距不是“快一点”,是“快到不同维度”。
写二分查找最大的坑是边界条件。是用left < right还是left <= right?mid要不要加一减一?这些细节新手非常容易写错,一旦错了就是死循环或者漏掉目标值。我的建议是记住一组固定搭配:左闭右闭区间(也就是left和right都包含在查找范围内),while left <= right,更新时left = mid + 1、right = mid - 1。这套组合不容易出边界bug,适合用来建立正确直觉。
对于新手,二分查找还有一个额外价值:它让你第一次意识到,算法的选择高度依赖数据特征。遇到“有序数组查找”你就该想到二分,遇到“无序数组查找”你就算排序的成本划不划算再做决定。这种“审题—锁定数据特征—匹配算法”的流程,是所有算法题解法的通用框架。
3. 排序算法:算法入门的必修课
3.1 冒泡排序:拿来理解双层循环
几乎所有人的排序启蒙都是冒泡排序。算法每次比较相邻的两个元素,顺序不对就交换,一轮走完,最大的数就像气泡一样“冒”到数组末尾。然后忽略已排好的末尾元素,继续下一轮。
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] return arr冒泡排序的时间复杂度是 O(n²),数据量一大就会明显变慢。但它的教学价值不在效率,而在于它是理解“双层循环+交换条件”的最直观模型。很多初学者写不出双层循环的代码逻辑,冒泡排序就是最好的训练场:外层控制“进行几轮”,内层控制“这一轮比较到哪”。
我自己当初学冒泡的时候,犯过所有新手都会犯的错:忘记n - 1 - i这个减i,导致每一轮都多比较了几个已经有序的元素。这个错误不影响结果正确性,但会增加无用的比较次数。加了减i的优化之后,每一轮的内层比较范围都会缩短,严谨性和性能同时到位。
冒泡排序还有一个经典的优化点:如果在某一轮循环中没有发生任何交换,说明数组已经有序,可以直接提前结束。这个优化思路属于“基于现实观察而改进算法”,比盲目套模板有价值得多,因为它训练的是算法优化的思维模式。
3.2 选择排序和插入排序:不同思路的对比
了解冒泡之后,紧接着学选择排序和插入排序,你就会发现一个有意思的现象:同样排序一组数据,不同算法的“工作方式”完全不同。
选择排序的思路是:每一轮从未排序区域里面找出最小元素,把它放到已排序区域的末尾。
def selection_sort(arr): n = len(arr) for i in range(n): min_index = i for j in range(i + 1, n): if arr[j] < arr[min_index]: min_index = j arr[i], arr[min_index] = arr[min_index], arr[i] return arr选择排序和冒泡排序的时间复杂度都是 O(n²),但选择排序把“交换”操作减少到了每轮一次,在交换代价高(比如排序的是一组庞大对象,交换的是引用)的场景里更实用。
插入排序则更像打牌时整理手牌:每次拿一张新的牌,插入到手里已经排好的牌堆中的正确位置。
def insertion_sort(arr): for i in range(1, len(arr)): key = arr[i] j = i - 1 while j >= 0 and arr[j] > key: arr[j + 1] = arr[j] j -= 1 arr[j + 1] = key return arr插入排序在数据规模小、且基本有序的情况下表现非常好,甚至比一些复杂度更低的算法更快。原因是它的常数因子小,而且对“近乎有序”的数据天然友好。很多现代排序算法(包括“快排”的某些实现)在数据量小于某个阈值时会切换到插入排序,就是看中它在小规模数据下的效率优势。
初学者把这三个排序算法放在一起对比学习,比挨个单独学效果要好得多。对比会让你看到:同样是嵌套循环,循环变量的含义完全不同;同样是交换元素,交换次数和条件各不相同。这个“对比学习”的方法,可以继续沿用到后面所有算法主题上。
4. 递归:理解函数自己调用自己
4.1 递归的两个关键点:终止条件与递归步骤
递归是算法入门路上的第一个“劝退点”。很多人觉得递归难懂,是因为试图在脑子里跟着函数一层层往里钻,最后把自己绕晕了。正确理解递归的方式不是跟踪全过程,而是抓住两个关键点:终止条件和递归步骤。
终止条件(也叫基线条件)决定递归什么时候停下;递归步骤决定如何把一个大问题拆成一个更小的同类问题。只要这两个点都成立,递归就是正确的。至于中间那十几层栈的展开过程,根本不需要去手工模拟。
最经典的递归案例是阶乘:
def factorial(n): if n <= 1: return 1 return n * factorial(n - 1)这个函数的终止条件是n <= 1,递归步骤是n * factorial(n - 1)。它正确表达了“n 的阶乘等于 n 乘以 n-1 的阶乘”这个数学定义。没有终止条件的递归会无限调用,直到系统栈溢出崩溃,这是新手最常犯的错误。
递归的学习价值在于,很多数据结构(树、图)和算法(快速排序、归并排序、深度优先搜索)天生就是递归定义的,一旦掌握了递归的思维方式,这些内容学起来会轻松得多。如果你发现自己在非递归场景里硬套递归,往往是把简单问题复杂化了。
4.2 斐波那契数列与递归的代价
斐波那契数列是递归入门第二道经典题:每个数是前两个数之和(0、1、1、2、3、5、8、13……)。递归写法非常简洁:
def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)代码确实漂亮,但这个写法有一个严重的性能问题:大量重复计算。fib(5)会调用fib(4)和fib(3),fib(4)又会调用fib(3)和fib(2)——fib(3)被重复计算了两遍,往下展开重复更多。这种重复计算的规模是指数级的,算fib(40)就已经明显停顿,算fib(50)会慢到让人怀疑人生。
这说明一个道理:递归是解决问题的思路之一,但不是所有递归实现都是好算法。遇到这种重复计算,常见的优化手段是“记忆化”:用一个字典把已经算过的结果存起来,下次直接拿。
memo = {} def fib_memo(n): if n <= 1: return n if n in memo: return memo[n] memo[n] = fib_memo(n - 1) + fib_memo(n - 2) return memo[n]加了这一层缓存,fib(50)几乎瞬间出结果。同样一个问题,递归“原版”和“记忆化递归”的性能差异是分钟级与毫秒级的差距。这种“先能跑,再能快”的改进路径,是算法能力进阶的核心方式。入门阶段能亲手体会到这种差距,比听我说一百句“算法很重要”都管用。
5. 把自己学的写成博客
5.1 一篇算法博客应该包含什么
写博客的过程,其实是很好的二次学习。我在写完上面几个算法的代码后有个体会:能跑通代码和能讲清楚原理,完全是两个层级。讲清楚的过程会逼着你填补那些“以为自己知道、实际说不出来”的漏洞。
一篇合格的算法入门博客,最少应该包含下面几块内容:
- 问题定义:这个算法解决什么问题,输入输出是什么,场景是什么。
- 思路拆解:用自己的话描述算法怎么思考的,可以配生活化类比。
- 代码实现:标注清楚语言,代码要简单直白,不要炫技。
- 复杂度分析:时间和空间复杂度分别是什么,为什么是这个复杂度。
- 边界条件:哪些输入会导致算法出问题,比如空数组、单元素数组、目标值不存在。
- 与其他算法的对比:它和同类算法比,各自的适用场景是什么。
写博客的时候还有一个容易被忽视的细节:代码的缩进和格式一定要用代码块包好。很多新手直接在正文里粘贴代码,缩进全乱,读者根本没法看。每种代码块的语法高亮和缩进样式都是确定的,发布前自己预览一遍,确保代码格式不乱、没有乱码,这是对读者最起码的尊重。
5.2 新手写技术博客的避坑指南
作为第一次写技术博客的博主,踩过的坑说几个真实的:
第一坑是“想写大而全的教程”。我一开始曾经尝试把排序算法、查找算法、树、图、动态规划全部写进一篇文章,结果写了三天才写完大纲,最后发现根本收不住。第一篇博客的正确做法是缩小范围,聚焦在几个核心算法上,写深写透,而不是广撒网。“简单算法入门”这个选题本身就意味着范围收敛:不追求覆盖所有算法,只要把最基础、最核心的几个讲明白,就算成功。
第二坑是“只贴代码不解释思路”。代码是算法的实现,不是算法本身。如果你把代码贴上去就算完事,读者能复制能运行,但还是学不会。必须把代码背后的核心思路用文字讲透,为什么这么写、为什么这个条件写在前面、为什么这个循环从1开始而不是0,这些细节才是博客真正的价值。
第三坑是“忽略了读者的基础”。新手写博客最容易犯的错,就是想当然地认为读者懂的跟自己一样多。实际来搜“算法入门”的读者,往往连数组和列表的区别都还模糊。我写了一段时间后养成了一个习惯:每写到一个术语,就想一下“如果三年前的我来读,能不能看懂”。这招特别有效,能快速帮你调整表述的深浅。
还有一个技术上的建议:多用可运行的完整示例,不要只写核心片段。核心片段对于懂的人来说足够,但入门读者往往不清楚怎么补上缺失的上下文。我在这篇文章里的每个示例都提供了完整可运行的代码,读者复制到编辑器里直接跑,这种体验对新手友好得多。
6. 入门常见问题速查与避坑技巧
6.1 常见问题清单
算法入门的过程中,几乎每个人都会遇到类似的几个坑。我整理了一份快速排查表,你卡住的时候可以对照着找原因。
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 代码运行结果不对,毫无规律 | 循环边界条件写错,多循环了一轮或者少循环了一轮 | 用单元素数组、空数组、完全逆序数组跑测试用例 |
| 程序一直运行不结束 | 递归缺少终止条件,或者循环条件永远为真 | 逐个检查终止条件,打印中间变量看循环走到了哪里 |
| 数据量大时程序卡死 | 使用了 O(n²) 或更高复杂度的算法 | 换用 O(n log n) 或 O(log n) 算法,或者优化数据预处理 |
| 能跑出结果但和预期差一个位置 | 索引边界理解有偏差,二分查找中更新区间时边界处理有误 | 画一个长度为 5 的数组,手动模拟每一步的指针变化 |
| 写了递归但很难调通 | 把递归当成循环来用,试图在函数内部维护太多外部状态 | 重新审视递归的基线条件,确定每次递归调用都在缩小问题规模 |
这个表格里的每一项,都是我实际调试过程中真真切切踩过的坑。特别是二分查找的边界问题,即使写过很多次,偶尔还是会写错。后来我养成一个习惯:凡是涉及边界的代码,都会写几个边界测试用例(空数组、单元素数组、目标值在两端),跑一遍再提交。这个习惯救了我很多次。
6.2 我的几点实操心得
刚开始学算法的时候,我走过很长的弯路。现在回头看,有几个细节如果早一点知道,能节省非常多时间。
第一,不要纠结于一次看懂。算法的理解是螺旋式的,第一遍看不懂很正常。我学二分查找时,第一遍完全没理解为什么mid的计算要那么写,直接放弃去学别的内容了。过了一周重新看,突然就通了。大脑在你休息的时候也在消化知识,所以卡住了就放一放,换换脑子,比死磕高效得多。
第二,动手写比看代码重要一百倍。看十遍代码不如自己敲一遍。哪怕你敲的时候是把别人的代码对着抄下来,运行、调试、改bug这个过程,也比只看不写学到的东西多得多。我强烈建议:每学一个算法,合上教程,自己从零写一遍,写不出来再看。这个过程对记忆和理解的强化效果极其显著。
第三,建立自己的测试清单。学完一个算法之后,至少用三种输入去测:随机数组、完全逆序数组、已经有序的数组。这三个输入能覆盖大部分边界情况,不管是排序还是查找,跑完这三种输入你就能基本确认代码的正确性。
第四,用博客记录学习过程,是强者之路。把每个算法写成文章的过程,等于逼自己把“模糊的直觉”变成“清晰的解释”。很多我自以为掌握了的算法,都是在写博客的时候才意识到某个细节根本没有想透。这种“输出倒逼输入”的学习方式,远比一遍遍刷视频课有效。
用大白话总结一句我自己的体会:算法入门不是数学竞赛,不要被“高手”“大佬”的光环吓退。核心就是把最基础的几个算法吃透,把复杂度分析搞明白,再把刷题练手变成日常习惯。这篇文章写到这里,我自己其实也重新梳理了一遍很多细节,这篇“第一篇博客”是一个开始,后面我会继续更新更有深度的算法解析和数据结构内容。