Skip to content

算法思维入门

前言

如何高效地解决问题? 你可能遇到过这样的困惑:同一个问题,有人写的代码跑几秒就出结果,有人写的跑几分钟还在转。差别往往在于算法。本章带你理解算法的核心思维方式。

这篇文章会带你学什么?

学完这章后,你将获得:

  • 问题拆解能力:面对复杂问题,能想到用分治、递归等策略拆解,而不是一上来就写代码
  • 效率判断能力:用大 O 表示法判断两种解法哪个更高效,而不是凭感觉猜测
  • 复杂度思维:写代码前先估算数据规模和时间要求,选择合适的算法级别
  • 后续学习基础:为高级数据结构、分布式系统、机器学习打下基础
章节内容核心概念
第 1 章二分查找分治思想、O(log n)
第 2 章排序算法冒泡、快排、归并
第 3 章复杂度分析时间复杂度、空间复杂度

0. 全景图:算法是什么?

想象你要在一本字典里找一个单词:

  • 方法一:从第一页开始,一页一页翻(线性查找)
  • 方法二:根据首字母定位,再二分查找(二分查找)

两种方法都能找到,但效率天差地别。算法就是解决问题的方法

算法思维:解决问题的方法不同策略解决不同类型的问题
二分查找每次排除一半,O(log n)
时间复杂度速查
O(1)常数最优,如数组访问
O(log n)对数很好,如二分查找
O(n)线性一般,如遍历
O(n log n)线性对数可接受,如快速排序
O(n²)平方较慢,如冒泡排序
O(2ⁿ)指数很慢,如暴力递归
核心思想:算法是解决问题的方法。好的算法能让程序效率提升几个数量级。理解算法思维,比记住具体算法更重要。

算法的核心指标:

指标含义为什么重要
时间复杂度运行时间随数据量增长的趋势预测大规模数据的性能
空间复杂度内存占用随数据量增长的趋势评估内存消耗
正确性是否总能得到正确结果算法的基本要求

📊 逐行解读这张表

时间复杂度:用大 O 表示法描述。O(n) 表示数据量翻倍,时间翻倍;O(n²) 表示数据量翻倍,时间变成 4 倍。

空间复杂度:同样用大 O 表示法。有些算法用空间换时间(如哈希表),有些用时间换空间(如压缩算法)。

正确性:算法必须对所有可能的输入都能给出正确结果。边界条件(空输入、极大输入)最容易出错。


1. 二分查找:每次排除一半

1.1 二分查找的原理

💡 二分查找如何工作?

前提:数据必须有序

过程

  1. 找到中间元素
  2. 如果中间元素等于目标,找到!
  3. 如果目标小于中间元素,在左半部分继续
  4. 如果目标大于中间元素,在右半部分继续
  5. 每次排除一半,直到找到或确定不存在

时间复杂度:O(log n)

生活类比:猜数字游戏。我想一个 1-100 的数,你每次猜中间,我告诉你大了还是小了。最多猜 7 次就能猜中(因为 2⁷ = 128 > 100)。

👇 动手试试看: 下面这个演示展示了二分查找的工作原理,你可以选择顺序查找或二分查找来对比:

查找算法如何在数据中找到目标
顺序查找:一个一个找
3
7
2
9
5
1
8
4
6
10
目标数字:
时间复杂度:O(n)
适用:无序数组
性能对比
数据量顺序查找二分查找
10最多 10 次最多 4 次
100最多 100 次最多 7 次
1000最多 1000 次最多 10 次
10000最多 10000 次最多 14 次

1.2 为什么二分查找这么快?

数据量线性查找二分查找
100100 次7 次
1,0001,000 次10 次
1,000,0001,000,000 次20 次
1,000,000,0001,000,000,000 次30 次

� 逐行解读这张表

第一列(数据量):要查找的数据有多少。可以看到数据量从 100 增长到 10 亿(扩大了 1000 万倍!)

第二列(线性查找):最"笨"的方法,从第一个开始一个一个找。查找次数等于数据量,数据量越大,查找次数越多。

第三列(二分查找):聪明的方法,每次排除一半。查找次数只和数据量的对数有关,即使 10 亿数据也只需要 30 次!

对比结论:当数据量达到 100 万时,线性查找需要 100 万次,二分查找只需要 20 次——差距达 5 万倍!

� 对数增长的威力

二分查找的时间复杂度是 O(log n),这意味着:

  • 10 亿数据,最多查找 30 次
  • 1 万亿数据,最多查找 40 次

这就是对数增长的威力——数据量增加 1000 倍,查找次数只增加 10 次。


2. 排序:将无序变有序

2.1 常见排序算法

算法时间复杂度特点适用场景
冒泡排序O(n²)简单但慢教学、小数据量
选择排序O(n²)简单但慢小数据量
插入排序O(n²)对近乎有序的数据快小数据、近乎有序
快速排序O(n log n)实际最快通用排序
归并排序O(n log n)稳定排序需要稳定性的场景
堆排序O(n log n)原地排序内存受限场景

📊 逐行解读这张表

冒泡排序:最基础的排序算法,就像水底的气泡往上冒一样。简单易懂,但速度最慢。适合学习排序思想,不适合实际使用。

选择排序:每次选出最小的放到前面。也很简单,但无论数据是否有序都要做同样多的比较。

插入排序:像打扑克牌时整理手牌一样。把每个元素插入到前面已经排好序的部分中。对近乎有序的数据效率很高。

快速排序:实际开发中最常用的排序。平均情况下最快,但最坏情况(数据已经有序)会退化到 O(n²)。

归并排序:采用"分而治之"的思想,总是 O(n log n),但需要额外空间。适合需要稳定排序的场景。

堆排序:利用堆这种数据结构的排序,原地排序(不需要额外空间),但实际运行往往比快速排序慢。

2.2 为什么快速排序"快"?

💡 快速排序的原理

核心思想:分治法

  1. 选一个"基准"元素
  2. 把比基准小的放左边,比基准大的放右边
  3. 对左右两部分递归排序
  4. 合并结果

为什么快?

  • 每次划分后,基准元素就到了最终位置
  • 平均情况下,每次划分大约排除一半元素
  • 时间复杂度 O(n log n)

生活类比:整理书架。先抽出一本书,把比它薄的放左边,比它厚的放右边。然后对左右两堆分别重复这个过程。

👇 动手试试看: 下面这个演示展示了排序算法的可视化,你可以生成数组,观察冒泡排序和快速排序的过程对比:

排序算法把数据按顺序排列
50
30
70
40
90
20
60
80
10
55
请选择排序算法
选择一个排序算法开始演示
时间复杂度:
算法对比
算法平均时间最坏时间空间稳定
冒泡排序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²)O(n²)O(1)

3. 递归:自己调用自己

3.1 递归的本质

💡 什么是递归?

递归是函数调用自身的编程技巧。

两个关键要素

  1. 基本情况:什么时候停止递归?
  2. 递归步骤:如何把问题分解成更小的子问题?

经典例子:阶乘

js
function factorial(n) {
  if (n <= 1) return 1        // 基本情况
  return n * factorial(n - 1) // 递归步骤
}

生活类比:俄罗斯套娃。打开一个娃娃,里面是更小的娃娃,直到最小的那个打不开为止。

3.2 递归 vs 迭代

特性递归迭代(循环)
代码简洁度通常更简洁可能更复杂
内存消耗较高(调用栈)较低
性能稍慢(函数调用开销)更快
适用场景树遍历、分治算法简单重复任务

📊 逐行解读这张表

代码简洁度:递归通常只需要几行代码就能表达复杂的逻辑(如遍历树结构),而用循环可能需要更多的变量和嵌套。

内存消耗:递归会使用"调用栈"来保存每一层的信息,就像叠盘子一样,每递归一层就多一个盘子。循环则不需要这种开销。

性能:每次函数调用都有开销(参数传递、栈操作等),所以递归通常比循环慢一些。

适用场景:递归擅长处理本身就是递归结构的问题(如文件树、DOM 树);循环擅长简单的重复操作(如遍历数组)。

⚠️ 递归的陷阱

栈溢出:递归层次太深,调用栈空间耗尽。

解决方法

  • 改用迭代
  • 使用尾递归优化(某些语言支持)
  • 限制递归深度

👇 动手试试看: 下面这个演示展示了递归的调用过程,观察函数如何自己调用自己:

递归思维:自己调用自己把大问题分解成相同的小问题
🪆
俄罗斯套娃
打开一个大娃娃,里面有个小一点的娃娃
再打开还有更小的...直到最小的一个
这就是递归!
递归示例
阶乘:n! = n × (n-1)!
5! = 5 × 4!
4! = 4 × 3!
3! = 3 × 2!
2! = 2 × 1!
1! = 1 (基准情况)
↑ 返回 1
↑ 返回 2 × 1 = 2
↑ 返回 3 × 2 = 6
↑ 返回 4 × 6 = 24
↑ 返回 5 × 24 = 120
递归的三要素
1
基准情况
什么时候停止递归?必须有一个终止条件
例:n! 中 1! = 1
2
递归调用
如何让问题规模变小?调用自己处理更小的规模
例:n! 转换成 (n-1)!
3
返回结果
如何利用子问题的结果解决当前问题?
例:n × (n-1)! 的结果
✓ 优点
  • 代码简洁优雅
  • 自然表达递归结构
  • 适合树和图的遍历
✗ 缺点
  • 可能重复计算
  • 栈空间消耗大
  • 调试较困难

4. 贪心算法:每步选最优

4.1 贪心的思想

💡 什么是贪心算法?

贪心算法在每一步都选择当前看起来最优的选择,希望最终得到全局最优解。

适用条件

  1. 贪心选择性质:局部最优能导致全局最优
  2. 最优子结构:问题的最优解包含子问题的最优解

经典例子:硬币找零

  • 目标:用最少的硬币凑出指定金额
  • 贪心策略:每次选最大的硬币
  • 结果:67 元 = 50 + 10 + 5 + 1 + 1(5 枚)

生活类比:登山时,每次都选最陡的路往上走。虽然不一定能到最高峰,但通常能到不错的位置。

4.2 贪心的局限性

⚠️ 贪心不一定得到最优解

反例:硬币找零

如果硬币面值是 [1, 3, 4],要凑 6 元:

  • 贪心:4 + 1 + 1 = 3 枚
  • 最优:3 + 3 = 2 枚

贪心算法在这里失败了!

教训:贪心算法简单高效,但不总是能得到最优解。使用前要证明问题满足贪心条件。

👇 动手试试看: 下面这个演示展示了贪心算法的实际效果,你可以尝试不同的硬币组合,观察贪心策略的表现:

贪心算法:每步都选当前最优局部最优 → 全局最优?
贪心算法在每一步选择中都采取当前状态下最优的选择
希望通过一系列局部最优选择达到全局最优
经典问题
找零钱问题
需要找零:37
20元
× 1 = 20元
10元
× 1 = 10元
5元
× 1 = 5元
1元
× 2 = 2元
共需要 5 个硬币
✓ 贪心策略:每次选择面值最大的硬币
✓ 适用于人民币、美元等货币系统
贪心 vs 动态规划
特点贪心算法动态规划
决策方式每步选当前最优考虑所有可能,选最优
最优性可能不是全局最优保证全局最优
时间复杂度O(n) 或 O(n log n)O(n²) 或更高
适用场景局部最优 → 全局最优重叠子问题
✓ 优点
  • 实现简单
  • 效率高
  • 空间复杂度低
✗ 缺点
  • 不保证全局最优
  • 适用范围有限
  • 需要证明最优性

5. 算法设计范式

范式思想典型算法适用问题
分治把问题分解成小问题快速排序、归并排序可分解的问题
贪心每步选最优最小生成树、霍夫曼编码有贪心性质的问题
动态规划记录子问题的解背包问题、最短路径有重叠子问题
回溯试错,走不通就回退八皇后、全排列搜索问题

📊 逐行解读这张表

分治:把大问题拆成小问题,分别解决后再合并。就像整理房间,先分成客厅、卧室、厨房分别打扫,最后整体整洁。

贪心:每步都选当前最好的,不考虑长远后果。像吃饭时先挑最喜欢吃的菜,可能不是最优的吃法,但速度快。

动态规划:记住中间结果,避免重复计算。像记笔记,下次遇到同样问题直接查答案,不用重新推导。

回溯:走不通就退回来重试。像走迷宫,此路不通就返回上一个路口尝试别的路。

👇 动手试试看: 下面这个演示展示了不同算法设计范式的特点和应用场景:

算法设计范式解决问题的常用套路
算法设计范式是解决问题的通用策略,掌握这些套路可以快速找到解题思路
✂️
分治法
分而治之
📊
动态规划
保存结果避免重复
🎯
贪心法
局部最优
🔙
回溯法
试错法
✂️分治法
核心思想
将大问题分解成多个小问题,递归解决小问题,最后合并结果
适用场景
数组排序矩阵乘法大整数运算
经典问题
📝
归并排序
📝
快速排序
📝
二分查找
📝
Strassen 矩阵乘法
时间复杂度
O(n log n)
通常比暴力法快很多
范式对比总结
范式核心策略最优性适用场景
✂️ 分治法分解 → 递归 → 合并保证最优问题可独立分解
📊 动态规划保存子问题解保证最优有重叠子问题
🎯 贪心法每次选最优不一定最优局部最优 → 全局最优
🔙 回溯法深度优先搜索保证最优解空间小,需要穷举
如何选择合适的范式?
1
分析问题特征
是否有重叠子问题?是否有最优子结构?
2
判断是否需要最优解
贪心不一定最优,动态规划保证最优
3
考虑数据规模
回溯适合小规模,分治适合大规模

6. 总结:算法是解决问题的艺术

让我们用一个比喻总结各种算法思想:

思想比喻核心要点
二分查找猜数字每次排除一半
排序整理书架建立秩序
递归俄罗斯套娃化大为小
贪心登山选路局部最优

💡 核心启示

算法的本质是"效率"和"正确性"的平衡。

  • 好的算法能让程序效率提升几个数量级
  • 但过度优化可能引入复杂性
  • 先保证正确,再追求效率

理解算法思维,比记住具体算法更重要:

  • 分治:把大问题分解成小问题
  • 贪心:每步选最优
  • 动态规划:记录子问题的解
  • 回溯:试错,走不通就回退

延伸阅读

  • 算法导论:系统学习算法的经典教材
  • LeetCode:通过刷题提升算法能力
  • 算法可视化:直观理解算法执行过程
  • 竞赛算法:学习更高级的算法技巧