算法思维入门:从二分搜索到分治——easy-vibe 计算机基础附录精讲
【免费下载链接】easy-vibe💻 vibe coding 101|The first course for AI-native product builders.项目地址: https://gitcode.com/GitHub_Trending/ea/easy-vibe
本文基于 easy-vibe 开源项目(vibe coding 101|面向 AI 原生产品构建者的第一门课)日文版文档 docs/ja-jp/appendix/1-computer-fundamentals/algorithm-thinking.md 编写,是该课程"计算机基础"附录中关于算法思维的核心章节。文章完整继承原文档的概念、对照表、代码示例与实操演示,并补充复杂度分析、可运行的算法实现与仓库内的关联学习路径,帮助 AI 时代的开发者建立可迁移的算法心智。
你能从本文获得什么:
- 问题分解能力:面对复杂问题不再急于动手写代码,而是用分治、递归等策略先拆解问题;
- 效率判断能力:用大 O 记法判断两个方案哪个更高效,告别"凭感觉猜";
- 计算量思维:写代码之前先估算数据规模与时间需求,选择合适量级的算法;
- 后续学习基础:为高级数据结构、分布式系统、机器学习等内容打好地基。
| 章节 | 内容 | 核心概念 |
|---|---|---|
| 第 1 章 | 二分搜索 | 分治思想、O(log n) |
| 第 2 章 | 排序算法 | 冒泡排序、快速排序、归并排序 |
| 第 3 章 | 复杂度分析 | 时间复杂度、空间复杂度 |
0. 全景图:算法的本质是什么
想象在词典里查一个词:
- 方法 1:从第一页开始一页一页翻(线性搜索);
- 方法 2:先按首字母定位到章节,再在章节内二分查找(二分搜索)。
两种方法都能找到,但效率天差地别。算法,就是解决问题的方法——同样的输入,不同的方法决定了是"几秒出结果"还是"几分钟还在转"。
算法的三个核心指标
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 时间复杂度 | 数据量增长时,运行时间如何增长 | 预测大规模数据下的性能 |
| 空间复杂度 | 数据量增长时,内存占用如何增长 | 评估内存消耗 |
| 正确性 | 是否总能得到正确结果 | 算法的基本要求 |
逐项说明:
- 时间复杂度用大 O 记法描述。O(n) 表示数据量翻倍、时间也翻倍;O(n²) 表示数据量翻倍、时间变为 4 倍。
- 空间复杂度同样使用大 O 记法。有的算法用空间换时间(如哈希表),有的用时间换空间(如压缩算法)。
- 正确性要求算法对所有可能的输入都给出正确结果。边界条件——空输入、超大输入——恰恰是最容易出错的环节。
在 easy-vibe 仓库中,本章通过
<AlgorithmDemo />交互组件在文档站点中呈现搜索对比演示;从仓库 docs 目录结构可以确认,同一章节在 ar-sa、de-de、en、es-es、fr-fr、ja-jp、ko-kr、vi-vn、zh-cn、zh-tw 共 10 种语言版本中保持完全一致的内容骨架,便于多语言学习者对照阅读(参见 docs/en/appendix/1-computer-fundamentals/algorithm-thinking.md、docs/zh-cn/appendix/1-computer-fundamentals/algorithm-thinking.md)。
1. 二分搜索:每次排除一半
1.1 二分搜索的原理
前提:数据必须是已排序的。
执行步骤:
- 找到中间元素;
- 中间元素等于目标值——找到了!
- 目标值小于中间元素——继续在左半部分查找;
- 目标值大于中间元素——继续在右半部分查找;
- 每次都排除一半,直到找到目标或确认不存在。
时间复杂度:O(log n)。
生活类比:猜数字游戏。我心中想一个 1~100 的数字,你每次都猜中间值,我回答"大了/小了"。最多 7 次就能猜中——因为 2⁷ = 128 > 100。
一个标准的 JS 实现如下(对应原文档"每次排除一半"的步骤描述):
function binarySearch(sortedArr, target) { let left = 0 let right = sortedArr.length - 1 while (left <= right) { const mid = Math.floor((left + right) / 2) // 步骤 1:找中间元素 if (sortedArr[mid] === target) return mid // 步骤 2:命中 if (target < sortedArr[mid]) { right = mid - 1 // 步骤 3:去左半部分 } else { left = mid + 1 // 步骤 4:去右半部分 } } return -1 // 步骤 5:不存在 }原文档通过
<SearchAlgorithmDemo />组件提供可交互演示,你可以在线选择"线性搜索 / 二分搜索"对比两者的执行过程;该组件由文档站点的前端渲染层提供,各语言版本文档中以同一组件标签引用。
1.2 二分搜索的性能原理
| 数据量 | 线性搜索 | 二分搜索 |
|---|---|---|
| 100 | 100 次 | 7 次 |
| 1,000 | 1,000 次 | 10 次 |
| 1,000,000 | 1,000,000 次 | 20 次 |
| 1,000,000,000 | 1,000,000,000 次 | 30 次 |
逐列解读:
- 第 1 列(数据量):从 100 一路增长到 10 亿,扩大了 1000 万倍;
- 第 2 列(线性搜索):最"笨"的方法,从头到尾逐个找,搜索次数等于数据量;
- 第 3 列(二分搜索):聪明的方法,每次排除一半,搜索次数只与数据量的对数相关——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 快速排序的性能原理
核心思想:分治法。
- 选取一个"基准"(pivot)元素;
- 把比基准小的放左边,比基准大的放右边;
- 递归地对左右两部分排序;
- 合并结果。
为什么快?
- 每次分区后,基准元素就到达了它的最终位置;
- 平均情况下,每次分区大约排除一半元素;
- 时间复杂度 O(n log n)。
生活类比:整理书架。先抽出一本书,比它薄的放左边、比它厚的放右边,然后对左右两堆重复同样的操作。
参考实现(分区思路,对应原文档 4 个步骤):
function quickSort(arr) { if (arr.length <= 1) return arr // 递归出口 const pivot = arr[Math.floor(arr.length / 2)] // 步骤 1:选基准 const left = [], right = [] for (let i = 0; i < arr.length; i++) { if (i === Math.floor(arr.length / 2)) continue if (arr[i] < pivot) left.push(arr[i]) // 步骤 2:小的放左边 else right.push(arr[i]) // 步骤 2:大的放右边 } // 步骤 3:递归排序 return [...quickSort(left), pivot, ...quickSort(right)] // 步骤 4:合并 }原文档通过
<SortingAlgorithmDemo />组件提供排序过程可视化,你可以生成数组后对比冒泡排序与快速排序的完整执行过程。
3. 递归:调用自己
3.1 递归的原理
递归是函数调用自身的编程技巧。
两个关键要素:
- 基准情形(base case):什么时候停止递归?
- 递归步骤:如何把问题分解成更小的子问题?
经典例子:阶乘
function factorial(n) { if (n <= 1) return 1 // 基准情形 return n * factorial(n - 1) // 递归步骤 }生活类比:俄罗斯套娃。打开一个娃娃,里面还有更小的娃娃,直到最小的娃娃打不开为止。
3.2 递归 vs 迭代
| 特性 | 递归 | 迭代(循环) |
|---|---|---|
| 代码简洁度 | 通常更简洁 | 往往更复杂 |
| 内存消耗 | 高(调用栈) | 低 |
| 性能 | 略慢(函数调用开销) | 更快 |
| 适用场景 | 树遍历、分治算法 | 简单重复任务 |
逐项解读:
- 代码简洁度:递归通常几行代码就能表达复杂逻辑(如遍历树结构),而循环往往需要更多变量和嵌套。
- 内存消耗:递归用"调用栈"保存每一层的信息,像叠盘子,每深入一层就多叠一个盘子;循环没有这种开销。
- 性能:每次函数调用都有开销(参数传递、栈操作等),所以递归通常比循环略慢。
- 适用场景:递归擅长处理本身具有递归结构的问题(文件树、DOM 树),循环擅长简单重复操作(遍历数组)。
⚠️ 递归的陷阱
栈溢出:递归太深,调用栈空间耗尽。
解决办法:
- 改用迭代;
- 使用尾递归优化(部分语言支持);
- 限制递归深度。
原文档通过
<RecursiveThinkingDemo />组件可视化递归的调用过程,直观观察函数如何一层层调用自己。
4. 贪心算法:每一步选最优
4.1 贪心思想
贪心算法:每一步都做出当前看起来最优的选择,期望最终得到全局最优解。
适用条件:
- 贪心选择性质:局部最优能导向全局最优;
- 最优子结构:问题的最优解包含子问题的最优解。
经典例子:硬币找零
- 目标:用最少数量的硬币凑出指定金额;
- 贪心策略:每次都选面值最大的硬币;
- 结果:67 元 = 50 + 10 + 5 + 1 + 1(5 枚)。
生活类比:爬山时每次都选最陡的路往上爬。不一定能到达最高峰,但通常能到相当不错的位置。
4.2 ⚠️ 贪心并不总是最优
反例:硬币找零
假设硬币面值为 [1, 3, 4],要凑出 6 元:
- 贪心法:4 + 1 + 1 = 3 枚;
- 最优解:3 + 3 = 2 枚。
贪心算法在这里失败了!
教训:贪心算法简单高效,但并非总能得到最优解。使用之前必须证明问题满足贪心条件。比如上面这个反例,用代码验证会更直观:
// 贪心策略:每次都选最大面额 function greedyChange(coins, amount) { coins.sort((a, b) => b - a) // 从大到小 let count = 0 for (const c of coins) { while (amount >= c) { amount -= c count++ } } return amount === 0 ? count : Infinity } greedyChange([1, 3, 4], 6) // 得到 3,但真实最优是 3 + 3 = 2 枚原文档通过
<GreedyThinkingDemo />组件让你尝试不同的硬币组合,观察贪心策略的实际表现——当你试到 [1, 3, 4] 凑 6 时,就能亲眼看到贪心的失败。
5. 四大算法设计范式
| 范式 | 思想 | 代表算法 | 适合的问题 |
|---|---|---|---|
| 分治法 | 把问题分解成小问题 | 快速排序、归并排序 | 可分解的问题 |
| 贪心法 | 每次选最优 | 最小生成树、哈夫曼编码 | 具有贪心性质的问题 |
| 动态规划 | 记录子问题的解 | 背包问题、最短路径 | 有重叠子问题的问题 |
| 回溯法 | 试错,走不通就回退 | 八皇后、全排列 | 搜索问题 |
逐项解读:
- 分治法:把大问题拆成小问题、分别解决再合并。就像打扫房间,分成客厅、卧室、厨房分别打扫,最后整体干净了。
- 贪心法:每次选当前最好的,不考虑长远结果。就像吃饭时先吃最喜欢的菜,未必是最优吃法,但速度快。
- 动态规划:记忆中间结果,避免重复计算。就像做笔记,下次遇到同样的问题直接查答案,不用重新推导。
- 回溯法:走不通就回退重试。就像走迷宫,这条路不通就退回上一个岔路口换一条路。
动态规划的一个直观示例——斐波那契数列,正好演示"记录子问题的解"这一核心思想:
// 朴素递归:大量重复计算(重叠子问题) function fibNaive(n) { if (n <= 1) return n return fibNaive(n - 1) + fibNaive(n - 2) // 时间复杂度 O(2ⁿ) } // 动态规划:自底向上记录子问题结果 function fibDp(n) { if (n <= 1) return n const dp = [0, 1] for (let i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2] // 只算一次,后面直接查表 } return dp[n] // 时间复杂度 O(n) }原文档通过
<AlgorithmParadigmDemo />组件对比不同设计范式的特点与适用场景。
6. 复杂度分析入门:选对量级
原文档在章节总表中承诺了"第 3 章 计算量分析",并强调写代码前先估算数据规模与时间需求。这里把散落在各章中的复杂度知识汇总成一张速查表:
| 复杂度 | 名称 | 数据量翻倍后的表现 | 典型例子 |
|---|---|---|---|
| O(1) | 常数级 | 时间不变 | 哈希表查找、数组按下标访问 |
| O(log n) | 对数级 | 只增加固定次数 | 二分搜索(本章第 1 节) |
| O(n) | 线性级 | 时间翻倍 | 线性搜索、单次遍历 |
| O(n log n) | 线性对数级 | 略超翻倍 | 快速排序、归并排序(本章第 2 节) |
| O(n²) | 平方级 | 时间变为 4 倍 | 冒泡排序、嵌套循环 |
| O(2ⁿ) | 指数级 | 爆炸式增长 | 朴素递归斐波那契 |
估算三步法(对应原文档"计算量思维"目标):
- 先问数据规模:输入是 100、100 万还是 10 亿?这直接决定你需要的复杂度量级;
- 再定算法量级:O(n²) 算法在 100 万数据下意味着约 10¹² 次操作,几乎是不可接受的;
- 最后权衡空间:需要 O(n) 额外空间的算法(如归并排序)在内存受限的环境里可能不如原地排序(如堆排序)。
7. 总结:算法是解决问题的艺术
用比喻把各种算法思想收束起来:
| 思想 | 比喻 | 核心要点 |
|---|---|---|
| 二分搜索 | 猜数字 | 每次排除一半 |
| 排序 | 整理书架 | 建立秩序 |
| 递归 | 俄罗斯套娃 | 化大为小 |
| 贪心法 | 爬山选路 | 局部最优 |
核心启示:算法的本质是"效率"与"正确性"的平衡。
- 好算法能让程序效率产生数量级的提升;
- 但过度优化也可能带来不必要的复杂度;
- 先保证正确性,再追求效率。
理解算法思想比死记具体算法更重要:
- 分治:把大问题拆成小问题;
- 贪心:每次选最优;
- 动态规划:记录子问题的解;
- 回溯:试错,走不通就回头。
与 easy-vibe 课程体系的衔接
本章属于 easy-vibe 计算机基础附录(附录索引),建议按以下路径继续深入学习:
- 下一站·数据结构:算法离不开数据结构,"程序 = 数据结构 + 算法"。紧接本章的 数据结构序论 会讲解数组、链表、哈希表、树、图,以及它们与算法复杂度的关系——你在这里学到的二分搜索,正是有序数组/平衡树的查询基础;
- 关联·编程语言:编程语言 章节从语言层面解释递归、函数调用的底层机制,可与本章递归一节互相印证;
- 落地·Vibe Coding 全栈:Vibe Coding 全栈 展示如何把"问题分解 → 复杂度估算 → 编写实现"的算法思维应用到 AI 辅助的完整项目开发中;
- 语言对照:若需中文对照,可阅读 docs/zh-cn/appendix/1-computer-fundamentals/algorithm-thinking.md,英文版见 docs/en/appendix/1-computer-fundamentals/algorithm-thinking.md,其余语言版本位于 docs 下对应目录;
- 课程入口:完整的附录导读见 docs/ja-jp/guide/introduction.md。
参考资料
- 《算法导论》:系统学习算法的经典教材;
- LeetCode:通过刷题实战提升算法能力;
- 算法可视化工具:直观理解算法的执行过程(本仓库文档中的
<AlgorithmDemo />、<SearchAlgorithmDemo />、<SortingAlgorithmDemo />、<RecursiveThinkingDemo />、<GreedyThinkingDemo />、<AlgorithmParadigmDemo />交互组件即承担类似可视化作用); - 竞赛编程:学习更高级的算法技巧。
【免费下载链接】easy-vibe💻 vibe coding 101|The first course for AI-native product builders.项目地址: https://gitcode.com/GitHub_Trending/ea/easy-vibe
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考