第一篇博客我选了“简单算法入门”这个主题。原因很直白算法是编程的地基也是很多初学者卡住的第一道坎。我想把这块硬骨头拆开、炖烂写清楚“算法到底是什么”“为什么要学它”“学的时候会遇到哪些坑”让看完的人能真的动手写几个基础算法而不是只记住几个名词。这篇文章既是我自己的学习笔记整理成文也希望能帮到那些刚写完“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 rightmid要不要加一减一这些细节新手非常容易写错一旦错了就是死循环或者漏掉目标值。我的建议是记住一组固定搭配左闭右闭区间也就是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这个过程也比只看不写学到的东西多得多。我强烈建议每学一个算法合上教程自己从零写一遍写不出来再看。这个过程对记忆和理解的强化效果极其显著。第三建立自己的测试清单。学完一个算法之后至少用三种输入去测随机数组、完全逆序数组、已经有序的数组。这三个输入能覆盖大部分边界情况不管是排序还是查找跑完这三种输入你就能基本确认代码的正确性。第四用博客记录学习过程是强者之路。把每个算法写成文章的过程等于逼自己把“模糊的直觉”变成“清晰的解释”。很多我自以为掌握了的算法都是在写博客的时候才意识到某个细节根本没有想透。这种“输出倒逼输入”的学习方式远比一遍遍刷视频课有效。用大白话总结一句我自己的体会算法入门不是数学竞赛不要被“高手”“大佬”的光环吓退。核心就是把最基础的几个算法吃透把复杂度分析搞明白再把刷题练手变成日常习惯。这篇文章写到这里我自己其实也重新梳理了一遍很多细节这篇“第一篇博客”是一个开始后面我会继续更新更有深度的算法解析和数据结构内容。