通俗例子速懂核心算法
每个算法都对应生活场景,看完就能 get 核心逻辑:
一、基础排序与查找
- 快速排序:整理一筐苹果,先挑一个 “中间大小” 当标准,比它大的放右边、小的放左边,再分别整理两边,快又高效。
- 冒泡排序:排队时,相邻两人比身高,高的往后挪,像气泡往上冒,一步步把最高的排到最后,逻辑超简单。
- 二分查找:查字典找 “算” 字,先翻中间页码,看 “算” 在左边还是右边,再在半边里找中间,不用逐页翻。
- 哈希查找:用钥匙开家门,钥匙(数据)直接对应锁孔(存储位置),不用挨个试,一插就开。
二、复杂问题拆解
- 递归与分治:切生日蛋糕,先切成 8 块,再把每块分给一个人,大问题拆成小问题逐个解决。
- 动态规划:凑 10 元零钱,先想凑 5 元怎么凑,再在 5 元的基础上加面额,不用重复算已经试过的组合。
三、最优选择类
- 贪心算法:付账时找零,先拿 10 元、5 元的大面额,再补 1 元,一步选当下最省事的,不用纠结全局。
- 回溯算法:走迷宫,遇到死胡同就退回去换条路,直到找到出口,适合需要试错的场景。
四、数据结构与场景算法
- 链表 / 树遍历:逛超市找饮料,顺着货架一排一排看(链表),或先逛零食区再逛日用品区(树的分层遍历)。
- 图算法(最短路径):从家到公司,打开导航选红绿灯最少、距离最短的路,避开绕远的路线。
- 字符串匹配:在文章里找 “算法” 两个字,逐行扫描,遇到 “算” 就接着看后面是不是 “法”,精准定位。
- 枚举算法:试密码,密码是 3 位数字,从 000、001 一直试到 999,挨个排查所有可能。
- 模拟算法:玩模拟经营游戏,按游戏规则模拟开店、进货、赚钱的过程,还原真实场景的逻辑。
五、机器学习算法
- 分类算法:区分猫和狗,给算法看很多猫和狗的图片,它记住 “猫有尖耳朵、狗有长嘴巴”,再看到新图片就自动归类。
- 回归算法:预测房价,根据小区位置、面积、房龄这些信息,算出一个大概的价格范围。
- 聚类算法:整理衣柜,把上衣、裤子、裙子自动分成三类,不用手动标注,算法自己找相似点分组。
学习步骤
一、先打基础:明确核心概念与工具
- 掌握数据结构(算法的载体)
算法依赖数据结构,先理解核心结构的特性和操作(时间 / 空间复杂度):
-
- 线性结构:数组、链表、栈、队列(重点:增删查改的效率差异)
- 非线性结构:树(二叉树、BST、堆)、图(邻接矩阵 / 表)、哈希表(映射原理)
- 高级结构:跳表、并查集、前缀树(根据场景逐步深入)
工具:用你熟悉的语言(如 Python/Java)实现基础结构,理解底层逻辑(如链表的指针操作、哈希冲突解决)。
- 理解复杂度分析(判断算法优劣的核心)
-
- 时间复杂度:计算「操作次数与数据规模的关系」(O (1)、O (logn)、O (n)、O (nlogn)、O (n²) 等),重点分析循环和递归。
- 空间复杂度:额外占用的内存(如数组扩容、递归栈深度)。作用:刷题时先估算复杂度,避免写出超时 / 超内存的代码。
递归的通俗理解:
递归可不是 “自动循环” 哦,用大白话讲透特别简单,核心是「自己调用自己 + 有明确终点」,本质是 “拆小问题→解决小问题→合并结果”,只是执行过程看起来像循环而已~
先通俗理解递归:“分蛋糕” 的逻辑
拿之前的 cutPizza 举例,你要把 16 寸披萨分给 8 人,不用自己一刀刀切 8 块,而是做 3 件事:
- 把大披萨切成 2 个小披萨(16→8 寸),分给 4 人 + 4 人(拆小问题);
- 每个 8 寸披萨再切成 2 个 4 寸,分给 2 人 + 2 人(继续拆小);
- 直到披萨只分给 1 人(不用切了,这是 “终点”),再把所有小块的结果汇总。
这个过程里,“切披萨” 这个动作(函数)一直在重复,但每次处理的都是 “更小的披萨 + 更少的人”,直到触发 “1 人分整块” 的终点,才停止重复、回头合并结果 —— 这就是递归。
为啥会觉得递归像 “自动循环”?
因为递归和循环都在 “重复做某件事”,但逻辑完全不同:
- 循环是 “一直做同一个动作”(比如 for 循环反复 i++ 打印),是 “平着重复”;
- 递归是 “每次做的动作一样,但处理的对象变小了”(切大披萨→切小披萨),是 “层层深入后回头”,重复的是 “处理逻辑”,不是 “同一个场景”。
比如递归切披萨时,看似一直在 “切”,但每次切的披萨尺寸、分给的人数都在变,直到触达终点才停止,这个 “重复处理” 的过程就给人 “自动循环” 的感觉。
为什么第一次切,left和right不是应该是[8,4]吗,怎么直接【2,2】?
这是因为递归是「先深入到最底层,再逐层返回结果」的,不是 “切一次就直接得到中间结果”。咱们以 cutPizza(16, 8) 为例,一步步看 left 和 right 是怎么来的,就清楚了:
关键:递归的 “执行顺序” 是「先拆到底,再回头合并」
递归不是 “切一刀就停”,而是会一直切到最小单位(1 个人分),然后从最内层开始往回 “组装” 结果。就像剥洋葱,必须先剥到最里层,才能一层层往外拿。
具体过程:left 和 right 是怎么变成 [2,2] 的?
调用 cutPizza(16, 8) 时,第一次切割的 left 其实是 cutPizza(8, 4) 的结果,但 cutPizza(8,4) 不会直接返回 [8,4],而是会继续切割:
- 第一层:cutPizza (16, 8)
-
- 切出
half=8,然后调用left = cutPizza(8, 4)(左半块 8 寸分给 4 人)。 - 此时
left还没结果,必须先等cutPizza(8,4)算完。
- 切出
- 第二层:cutPizza (8, 4)
-
- 切出
half=4,调用left = cutPizza(4, 2)(左半块 4 寸分给 2 人)。 - 此时
left仍没结果,继续等cutPizza(4,2)算完。
- 切出
- 第三层:cutPizza (4, 2)
-
- 切出
half=2,调用left = cutPizza(2, 1)(左半块 2 寸分给 1 人)。 - 此时触发终止条件
people=1,直接返回[2](这是最内层的结果)。 - 同样,
right = cutPizza(2, 1)也返回[2]。 - 合并后,
cutPizza(4,2)返回[2, 2](这就是第三层的结果)。
- 切出
- 回到第二层:cutPizza (8,4)
-
- 此时
left拿到了cutPizza(4,2)的结果[2,2]。 - 同样,
right也是cutPizza(4,2)的结果[2,2]。 - 合并后,
cutPizza(8,4)返回[2,2,2,2](这是第二层的结果)。
- 此时
- 回到第一层:cutPizza (16,8)
-
- 此时
left拿到了cutPizza(8,4)的结果[2,2,2,2]。 - 同样,
right也是[2,2,2,2]。 - 合并后,最终返回
[2,2,2,2,2,2,2,2]。
- 此时
为什么第一次打印的 left 和 right 是 [2,2]?
因为 console.log 写在递归调用之后,会在最内层递归返回后才执行。比如第三层 cutPizza(4,2) 中,left 和 right 都是 [2],合并后打印的就是 [2] left 和 [2] right;然后第二层 cutPizza(8,4) 中,left 和 right 都是 [2,2],所以打印 [2,2] left 和 [2,2] right。
你看到的 [2,2] 其实是第三层递归返回的结果,而不是第一层直接切割的结果 —— 递归的 “深入” 和 “回溯” 特性,导致中间结果需要层层计算后才能得到。
通俗理解案例:6. 动态规划(凑零钱)
场景:用 1 元、5 元、10 元凑出 20 元,计算所有凑法 ``
// 动态规划凑零钱方法
function coinChange(coins, amount) {
const dp = Array(amount + 1).fill().map(() => []); dp[0] = [[]];
// 凑0元的方法:空数组
for (const coin of coins) {
for (let i = coin; i <= amount; i++) {
// 用当前硬币 + 凑(i-coin)元的方法
for (const prev of dp[i - coin]) { dp[i].push([...prev, coin]); } } }
// 去重(按从小到大排序后去重)
const unique = [...new Set(dp[amount].map(plan => plan.sort((a, b) => a - b).join(',')))];
return unique.map(plan => plan.split(',').map(Number)); }
// 调用示例:用1、5、10元凑20元 const coins = [1, 5, 10];
console.log("动态规划凑法:", coinChange(coins, 20));
// 输出:[[1,1,...1(20个)], [1*15,5], [1*10,5*2], ..., [10*2]](共10种)
这个 coinChange 函数用动态规划的思路,找出了用给定硬币凑出目标金额的所有不重复组合。我们以 coins = [1,5,10]、amount = 20 为例,用 “填表格” 的方式一步步拆解逻辑,特别简单直观:
核心思路:从 “凑小钱” 到 “凑大钱”
动态规划的关键是 “用已知解推未知解”。这里的逻辑是:
- 先算出 “凑 1 元、2 元... 直到 20 元” 的所有方法;
- 凑 i 元 的方法 = 用 1 枚硬币
coin+ 凑 i-coin 元 的所有方法(比如凑 5 元 = 用 1 枚 1 元 + 凑 4 元的方法,或用 1 枚 5 元 + 凑 0 元的方法)。
具体步骤:用表格 dp 记录所有凑法
dp 是一个数组,dp[i] 存储 “凑出 i 元的所有组合”(比如 dp[5] 存凑 5 元的方法)。
1. 初始化:凑 0 元的方法
dp[0] = [[]]; // 凑0元只有1种方法:不用任何硬币(空数组)
这是 “最基础的解”,所有其他金额的凑法都从这里推导出来。
2. 遍历每一种硬币,更新凑法
我们有硬币 1、5、10,依次用它们来扩展凑法:
第一步:用硬币 1 元扩展
遍历金额 i 从 1 到 20(因为 1 元是最小硬币,所有金额都能凑):
-
凑
i元的方法 = 用 1 枚 1 元 + 凑i-1元的方法。dp[1]:i=1,i-1=0,所以dp[1] = [ [].concat(1) ] → [[1]]dp[2]:i=2,i-1=1,所以dp[2] = [ [1].concat(1) ] → [[1,1]]- ... 以此类推...
dp[5]:此时会得到[[1,1,1,1,1]](5 个 1 元)。
第二步:用硬币 5 元扩展
遍历金额 i 从 5 到 20(5 元只能凑≥5 元的金额):
-
凑
i元的方法 = 用 1 枚 5 元 + 凑i-5元的方法(叠加到之前的方法上)。dp[5]:之前已有[[1,1,1,1,1]],现在加dp[0].concat(5) → [[]].concat(5) → [[5]],所以dp[5]变成[[1,1,1,1,1], [5]]dp[10]:之前只有 10 个 1 元[[1*10]],现在加dp[5].concat(5)→ 即[[1*5,5], [5,5]],所以dp[10]变成[[1*10], [1*5,5], [5,5]]- ... 以此类推,
dp[15]、dp[20]都会新增含 5 元的组合...
第三步:用硬币 10 元扩展
遍历金额 i 从 10 到 20(10 元只能凑≥10 元的金额):
-
凑
i元的方法 = 用 1 枚 10 元 + 凑i-10元的方法(继续叠加)。dp[10]:之前已有[[1*10], [1*5,5], [5,5]],现在加dp[0].concat(10) → [[10]],所以dp[10]变成[[1*10], [1*5,5], [5,5], [10]]dp[20]:之前已有 “纯 1 元、含 5 元” 的组合,现在加dp[10].concat(10)→ 即把dp[10]的每种方法加 1 枚 10 元,比如[1*10,10]、[1*5,5,10]等,最终得到所有含 10 元的组合...
3. 去重:避免重复组合
由于硬币顺序不影响结果(比如 [1,5] 和 [5,1] 是同一种凑法),需要去重:
- 先把每个组合排序(比如
[5,1]变成[1,5]),再用字符串去重(比如[1,5].join(',') → "1,5")。
最终结果:凑 20 元的 10 种方法
经过上述步骤,dp[20] 去重后会得到以下组合(按硬币从小到大排序):
- 20 个 1 元 →
[1,1,...,1](20 个) - 15 个 1 元 + 1 个 5 元 →
[1*15,5] - 10 个 1 元 + 2 个 5 元 →
[1*10,5,5] - 5 个 1 元 + 3 个 5 元 →
[1*5,5,5,5] - 4 个 5 元 →
[5,5,5,5] - 10 个 1 元 + 1 个 10 元 →
[1*10,10] - 5 个 1 元 + 1 个 5 元 + 1 个 10 元 →
[1*5,5,10] - 2 个 5 元 + 1 个 10 元 →
[5,5,10] - 0 个 1 元 + 2 个 10 元 →
[10,10] - 0 个 5 元 + 2 个 10 元 → (和第 9 种一致,去重后保留)
动态规划的核心在这里
- 表格
dp:像 “备忘录” 一样,记录每个小金额的所有凑法,避免重复计算; - 从小到大推导:从 0 元开始,用已知的小金额凑法,一步步算出大金额的凑法;
- 叠加硬币:每加入一种硬币,就用它扩展出更多凑法,最终覆盖所有可能性。
这种思路比 “暴力枚举” 高效得多,因为它 “不重复计算子问题”,而是用表格存储中间结果~