算法的通俗理解

92 阅读11分钟

通俗例子速懂核心算法

每个算法都对应生活场景,看完就能 get 核心逻辑:

一、基础排序与查找

  • 快速排序:整理一筐苹果,先挑一个 “中间大小” 当标准,比它大的放右边、小的放左边,再分别整理两边,快又高效。
  • 冒泡排序:排队时,相邻两人比身高,高的往后挪,像气泡往上冒,一步步把最高的排到最后,逻辑超简单。
  • 二分查找:查字典找 “算” 字,先翻中间页码,看 “算” 在左边还是右边,再在半边里找中间,不用逐页翻。
  • 哈希查找:用钥匙开家门,钥匙(数据)直接对应锁孔(存储位置),不用挨个试,一插就开。

二、复杂问题拆解

  • 递归与分治:切生日蛋糕,先切成 8 块,再把每块分给一个人,大问题拆成小问题逐个解决。
  • 动态规划:凑 10 元零钱,先想凑 5 元怎么凑,再在 5 元的基础上加面额,不用重复算已经试过的组合。

三、最优选择类

  • 贪心算法:付账时找零,先拿 10 元、5 元的大面额,再补 1 元,一步选当下最省事的,不用纠结全局。
  • 回溯算法:走迷宫,遇到死胡同就退回去换条路,直到找到出口,适合需要试错的场景。

四、数据结构与场景算法

  • 链表 / 树遍历:逛超市找饮料,顺着货架一排一排看(链表),或先逛零食区再逛日用品区(树的分层遍历)。
  • 图算法(最短路径):从家到公司,打开导航选红绿灯最少、距离最短的路,避开绕远的路线。
  • 字符串匹配:在文章里找 “算法” 两个字,逐行扫描,遇到 “算” 就接着看后面是不是 “法”,精准定位。
  • 枚举算法:试密码,密码是 3 位数字,从 000、001 一直试到 999,挨个排查所有可能。
  • 模拟算法:玩模拟经营游戏,按游戏规则模拟开店、进货、赚钱的过程,还原真实场景的逻辑。

五、机器学习算法

  • 分类算法:区分猫和狗,给算法看很多猫和狗的图片,它记住 “猫有尖耳朵、狗有长嘴巴”,再看到新图片就自动归类。
  • 回归算法:预测房价,根据小区位置、面积、房龄这些信息,算出一个大概的价格范围。
  • 聚类算法:整理衣柜,把上衣、裤子、裙子自动分成三类,不用手动标注,算法自己找相似点分组。

学习步骤

一、先打基础:明确核心概念与工具

  1. 掌握数据结构(算法的载体)

算法依赖数据结构,先理解核心结构的特性和操作(时间 / 空间复杂度):

    • 线性结构:数组、链表、栈、队列(重点:增删查改的效率差异)
    • 非线性结构:树(二叉树、BST、堆)、图(邻接矩阵 / 表)、哈希表(映射原理)
    • 高级结构:跳表、并查集、前缀树(根据场景逐步深入)

工具:用你熟悉的语言(如 Python/Java)实现基础结构,理解底层逻辑(如链表的指针操作、哈希冲突解决)。

  1. 理解复杂度分析(判断算法优劣的核心)
    • 时间复杂度:计算「操作次数与数据规模的关系」(O (1)、O (logn)、O (n)、O (nlogn)、O (n²) 等),重点分析循环和递归。
    • 空间复杂度:额外占用的内存(如数组扩容、递归栈深度)。作用:刷题时先估算复杂度,避免写出超时 / 超内存的代码。

递归的通俗理解:

递归可不是 “自动循环” 哦,用大白话讲透特别简单,核心是「自己调用自己 + 有明确终点」,本质是 “拆小问题→解决小问题→合并结果”,只是执行过程看起来像循环而已~

先通俗理解递归:“分蛋糕” 的逻辑

拿之前的 cutPizza 举例,你要把 16 寸披萨分给 8 人,不用自己一刀刀切 8 块,而是做 3 件事:

  1. 把大披萨切成 2 个小披萨(16→8 寸),分给 4 人 + 4 人(拆小问题);
  2. 每个 8 寸披萨再切成 2 个 4 寸,分给 2 人 + 2 人(继续拆小);
  3. 直到披萨只分给 1 人(不用切了,这是 “终点”),再把所有小块的结果汇总。

这个过程里,“切披萨” 这个动作(函数)一直在重复,但每次处理的都是 “更小的披萨 + 更少的人”,直到触发 “1 人分整块” 的终点,才停止重复、回头合并结果 —— 这就是递归。

为啥会觉得递归像 “自动循环”?

因为递归和循环都在 “重复做某件事”,但逻辑完全不同:

  • 循环是 “一直做同一个动作”(比如 for 循环反复 i++ 打印),是 “平着重复”;
  • 递归是 “每次做的动作一样,但处理的对象变小了”(切大披萨→切小披萨),是 “层层深入后回头”,重复的是 “处理逻辑”,不是 “同一个场景”。

比如递归切披萨时,看似一直在 “切”,但每次切的披萨尺寸、分给的人数都在变,直到触达终点才停止,这个 “重复处理” 的过程就给人 “自动循环” 的感觉。

为什么第一次切,left和right不是应该是[8,4]吗,怎么直接【2,2】?

这是因为递归是「先深入到最底层,再逐层返回结果」的,不是 “切一次就直接得到中间结果”。咱们以 cutPizza(16, 8) 为例,一步步看 leftright 是怎么来的,就清楚了:

关键:递归的 “执行顺序” 是「先拆到底,再回头合并」

递归不是 “切一刀就停”,而是会一直切到最小单位(1 个人分),然后从最内层开始往回 “组装” 结果。就像剥洋葱,必须先剥到最里层,才能一层层往外拿。

具体过程:leftright 是怎么变成 [2,2] 的?

调用 cutPizza(16, 8) 时,第一次切割的 left 其实是 cutPizza(8, 4) 的结果,但 cutPizza(8,4) 不会直接返回 [8,4],而是会继续切割:

  1. 第一层:cutPizza (16, 8)
    • 切出 half=8,然后调用 left = cutPizza(8, 4)(左半块 8 寸分给 4 人)。
    • 此时 left 还没结果,必须先等 cutPizza(8,4) 算完。
  1. 第二层:cutPizza (8, 4)
    • 切出 half=4,调用 left = cutPizza(4, 2)(左半块 4 寸分给 2 人)。
    • 此时 left 仍没结果,继续等 cutPizza(4,2) 算完。
  1. 第三层:cutPizza (4, 2)
    • 切出 half=2,调用 left = cutPizza(2, 1)(左半块 2 寸分给 1 人)。
    • 此时触发终止条件 people=1,直接返回 [2](这是最内层的结果)。
    • 同样,right = cutPizza(2, 1) 也返回 [2]
    • 合并后,cutPizza(4,2) 返回 [2, 2](这就是第三层的结果)。
  1. 回到第二层:cutPizza (8,4)
    • 此时 left 拿到了 cutPizza(4,2) 的结果 [2,2]
    • 同样,right 也是 cutPizza(4,2) 的结果 [2,2]
    • 合并后,cutPizza(8,4) 返回 [2,2,2,2](这是第二层的结果)。
  1. 回到第一层:cutPizza (16,8)
    • 此时 left 拿到了 cutPizza(8,4) 的结果 [2,2,2,2]
    • 同样,right 也是 [2,2,2,2]
    • 合并后,最终返回 [2,2,2,2,2,2,2,2]

为什么第一次打印的 leftright[2,2]

因为 console.log 写在递归调用之后,会在最内层递归返回后才执行。比如第三层 cutPizza(4,2) 中,leftright 都是 [2],合并后打印的就是 [2] left[2] right;然后第二层 cutPizza(8,4) 中,leftright 都是 [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=1i-1=0,所以 dp[1] = [ [].concat(1) ] → [[1]]
    • dp[2]i=2i-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] 去重后会得到以下组合(按硬币从小到大排序):

  1. 20 个 1 元 → [1,1,...,1](20 个)
  2. 15 个 1 元 + 1 个 5 元 → [1*15,5]
  3. 10 个 1 元 + 2 个 5 元 → [1*10,5,5]
  4. 5 个 1 元 + 3 个 5 元 → [1*5,5,5,5]
  5. 4 个 5 元 → [5,5,5,5]
  6. 10 个 1 元 + 1 个 10 元 → [1*10,10]
  7. 5 个 1 元 + 1 个 5 元 + 1 个 10 元 → [1*5,5,10]
  8. 2 个 5 元 + 1 个 10 元 → [5,5,10]
  9. 0 个 1 元 + 2 个 10 元 → [10,10]
  10. 0 个 5 元 + 2 个 10 元 → (和第 9 种一致,去重后保留)

动态规划的核心在这里

  • 表格 dp:像 “备忘录” 一样,记录每个小金额的所有凑法,避免重复计算;
  • 从小到大推导:从 0 元开始,用已知的小金额凑法,一步步算出大金额的凑法;
  • 叠加硬币:每加入一种硬币,就用它扩展出更多凑法,最终覆盖所有可能性。

这种思路比 “暴力枚举” 高效得多,因为它 “不重复计算子问题”,而是用表格存储中间结果~