20251019-JavaScript算法基础笔记

76 阅读57分钟

🧠 第1页:数据结构的理解(是什么、有哪几类、区别)

💬 1️⃣ 什么是数据结构?

数据结构 = “数据 + 关系 + 操作” 简单讲:它是“组织和管理数据的方式”

📦 举个例子: 想象你是个奶茶店老板,要保存顾客的订单。

  • 数组 就像你有一叠排好序的订单本(第1个、第2个、第3个)。
  • 链表 就像你用回形针把订单一张一张串起来(中间插新订单不影响整体顺序)。
  • 像叠奶茶杯(后放的先拿:后进先出)。
  • 队列 像排队买奶茶(先来的先买:先进先出)。
  • 像店长组织层级(店长→主管→员工)。
  • 像外卖配送地图(门店和骑手之间是“路线连接”)。

🗂️ 第2页:常见的数据结构类型

这页讲了几种常见的数据结构:

类型特点小白生活类比
数组 Array连续存储、下标访问快、插删慢一排储物柜(编号取物快,换位置麻烦)
链表 Linked List不连续、插删快、查找慢串珠子(插珠子容易,但数第几个要一个个找)
栈 Stack后进先出(LIFO)一摞碗(最后放的最先拿)
队列 Queue先进先出(FIFO)排队买票(先来先买)
树 Tree层级关系家谱结构或公司组织架构
图 Graph任意连接关系地铁线路图、社交网络
哈希表 Hash Table键值对存储,查找快电话簿(查名字立刻得号码)

🔍 第3页:数据结构的区别(考点重点)

数组 vs 链表

  • 数组:查找快(可直接按编号取),插入删慢。
  • 链表:插入删快(随便插珠子),查找慢(得一个个找)。

栈 vs 队列

  • 栈:后进先出 → 像洗碗机,最上面的碗先洗。
  • 队列:先进先出 → 像排队取奶茶。

树 vs 图

  • 树:层级明确(只能一条路径)。
  • 图:可以多条路径交叉(像地铁线互通)。

🌟 小口诀助记:

数快查,链快插; 栈叠叠,队排排; 树分支,图乱连。


🧩 第4页:算法的理解(是什么、特性)

💬 什么是算法?

算法 = 解决问题的一套明确步骤

🧋 举例: 你点奶茶 →

  1. 接单
  2. 加冰/不加冰
  3. 调料混合
  4. 摇一摇
  5. 打包

这就是一个“算法流程”。

📘 定义里强调三点:

  1. 输入(原料)
  2. 输出(结果)
  3. 有穷性(不能无限摇啊)

💡 算法的五大特性

特性解释生活类比
输入性有输入顾客要点单
输出性有输出做出奶茶
有穷性步骤有限不会永远摇
确定性每步明确不同店员结果一样
可行性能实现材料和设备都够用

🧮 第5页:算法的应用场景

🪄 典型应用

  • 排序:从小到大排队 → 像按杯号排奶茶。
  • 查找:查一个顾客订单 → 像找某个取餐号。
  • 搜索:地图找最短路线 → 外卖小哥找最快路线。
  • 图像处理:AI修图算法。
  • 推荐系统:算法帮你选你喜欢的奶茶口味。

💻 示例代码(第5页底部)

let list = ['Tom', 'Jerry', 'Amy'];
list.find((item) => item === 'Tom');

📘 解释:

  • list 就是一组顾客名单。

  • .find() 就像在名单里一个个找:

    “是不是Tom?不是?下一个。哦,这个是Tom,找到了!”

  • 结果输出:Tom

🧠 类比:就像奶茶店小哥一页页翻订单,直到看到“Tom”的名字为止。


🎯 记忆重点总结

类别一句话记忆
数据结构“放东西的方式”
算法“解决问题的步骤”
数组连续抽屉,快查慢改
链表串珠子,插删快查慢
后进先出,像叠碗
队列先进先出,像排队
层级分明
乱中有路,连接关系
算法特性有输入、有输出、有穷、确定、可行

🧮 第6页:时间复杂度 & 空间复杂度

💡 一、什么是“复杂度”?

复杂度,就是算法“有多耗资源”。

  • 时间复杂度:花了多少时间(执行次数)
  • 空间复杂度:占了多少内存空间

🍵 举个例子:

想象你在开奶茶店做奶茶:

  • 你做 1 杯奶茶要 3 步(拿杯→加料→打包), 做 10 杯就是 30 步。 → 时间复杂度随着数量变多而增加。
  • 你做奶茶需要 1 个杯子、1 根吸管。 不管多少人来买,都用“同样的机器”。 → 空间复杂度几乎不变。

📘 二、为什么要分析复杂度?

因为:

同样能“完成任务”的算法,有的像奶茶机自动化(快),有的像手摇版(慢)。

分析复杂度可以:

  • 选出更快的算法;
  • 判断性能瓶颈;
  • 优化代码结构。

🧠 第7页:时间复杂度的理解

💬 定义:

时间复杂度 = 算法执行次数随输入规模(n)的变化情况。

我们不看“具体时间”,而看“执行趋势”。


💹 图示理解(Big O 图)

图上有各种线:

  • O(1):平的直线(永远只执行一次) → 比如固定打印“你好”,无论几个人点奶茶都一样快。
  • O(log n):慢慢增长(对数) → 像查字典,不用翻全部,只翻几页。
  • O(n):线性增长 → 每个人都要做一杯奶茶。
  • O(n log n):稍快一点的增长 → 排序算法常见。
  • O(n²):平方级别 → 比如“每个人都和每个人打招呼”那种爆炸增长。

💡 举个生活类比:

算法复杂度举例奶茶店比喻
O(1)固定步骤打印今日菜单(1 次)
O(n)单循环每位顾客做 1 杯奶茶
O(n²)双循环每位顾客都和所有顾客合影
O(log n)二分查找查找名字在字母表中靠前或靠后(翻一半再看)
O(n log n)快速排序不全做一遍,有分工效率高

🧩 第8页:代码讲解(时间复杂度示例)

🧱 示例1:线性 O(n)

for (let i = 0; i < n; i++) {
  console.log(i);
}

👉 执行次数 = n 📘 就像有 n 个顾客,每人点一杯奶茶。


🧱 示例2:平方 O(n²)

for (let i = 0; i < n; i++) {
  for (let j = 0; j < n; j++) {
    console.log(i, j);
  }
}

👉 外层循环 n 次 × 内层循环 n 次 = n² 次 📘 就像每位顾客(i)都要和每个其他顾客(j)握手一次。 人数一多,炸掉 ⚠️


🧱 示例3:对数 O(log n)

for (let i = 1; i < n; i = i * 2) {
  console.log(i);
}

👉 每次都乘以2,执行次数变少。 📘 像“查字典”:一开始翻一半,不行再翻一半……很快找到。 (翻页次数少 → log n)


🧮 第9页:空间复杂度(存储占用)

📦 定义

空间复杂度衡量程序运行时占用的额外内存空间


🧱 示例代码

function testSpace(n) {
  let a = 0; // 占用1个空间
  let b = new Array(n).fill(0); // 占用n个空间
}

📘 a 是一个变量 → O(1) 📘 b 是一个长度为 n 的数组 → O(n)

🔑 所以这段代码的空间复杂度是 O(n)


🍵 生活类比

空间复杂度举例奶茶店比喻
O(1)用1个机器反复做奶茶一台奶茶机多次使用
O(n)每个顾客都配一台奶茶机空间消耗暴增
O(n²)每台奶茶机旁还放一台备份店面爆炸 💥

📊 第10页:时间复杂度 vs 空间复杂度 总结表

概念代表含义例子奶茶店类比
时间复杂度执行步骤随规模变化for循环、排序做奶茶所需的时间
空间复杂度占用存储空间多少数组、临时变量机器设备占地面积
O(1)固定步骤单次打印只用一个摇杯机
O(n)线性增长遍历数组每个顾客一杯奶茶
O(n²)平方增长双循环每个顾客都和别人互动
O(log n)对数增长二分查找不用翻完字典也能找到
O(n log n)混合型高级排序算法智能分工式做奶茶

🎯 快速记忆口诀:

时间看步骤,空间看杯数。 O(1) 定心稳,O(n) 一人一杯,O(n²) 店爆雷,O(log n) 翻半页。

🧮 第11页:集合(Set)的理解

💡 1. 什么是集合?

集合(Set)是一种“不重复元素的容器”。

📦 通俗讲: 它像一个「无重复奶茶订单盒」,每个口味只能出现一次。

const set = new Set([1, 2, 3, 3]);
console.log(set); // 输出: {1, 2, 3}

🧋 生活类比:

  • 顾客点单:如果有人点了两杯“珍珠奶茶”,系统只记一次。 (重复的不再加)

⚙️ 第12页:集合的常见操作

✳️ 四大基础操作

操作作用类比
add()添加元素加一个新的奶茶口味
delete()删除元素下架某个口味
has()检查是否存在看某口味是否还卖
clear()清空所有元素全部下架,重新来过

💻 示例 1:add()

let set = new Set();
set.add('珍珠奶茶');
set.add('奶盖绿茶');
console.log(set); // { '珍珠奶茶', '奶盖绿茶' }

👉 向集合中“加新品种”

🧋 像奶茶店上新菜单~“加一个珍珠奶茶,加一个奶盖绿茶”。


💻 示例 2:delete()

set.delete('珍珠奶茶');
console.log(set); // { '奶盖绿茶' }

👉 删除指定口味 🧋 顾客投诉“珍珠太多”,那我们就“下架珍珠奶茶”。


💻 示例 3:has()

console.log(set.has('奶盖绿茶')); // true

👉 判断是否有该元素 🧋 “菜单上还有奶盖绿茶吗?” → 有!✅


💻 示例 4:clear()

set.clear();
console.log(set); // {}

👉 清空所有元素 🧋 店里装修了,菜单全清空。😆


🎯 第13页:集合的高级操作

集合不仅能存放数据,还能做“集合运算”: 👉 并集、交集、差集。

📘 一、并集(Union)

把两个集合里的元素合并在一起,不重复。

let A = new Set([1, 2, 3]);
let B = new Set([3, 4, 5]);
let union = new Set([...A, ...B]);
console.log(union); // {1, 2, 3, 4, 5}

🧋 类比:

  • 店A卖:珍珠奶茶、芋圆奶茶
  • 店B卖:芋圆奶茶、抹茶拿铁 合并后 → 所有口味都上:珍珠、芋圆、抹茶。

📊 图中两个圆交叠:AB之间全填满。


📘 二、交集(Intersection)

取两个集合中都存在的元素

let intersection = new Set([...A].filter(x => B.has(x)));
console.log(intersection); // {3}

🧋 类比:

  • 店A卖【珍珠、芋圆、抹茶】
  • 店B卖【芋圆、红豆、抹茶】 → 两店都卖的 = 【芋圆、抹茶】。

📊 图中两个圆的重叠部分就是交集。


🧠 第14页:差集(Difference)

取“在A中但不在B中”的元素。

let difference = new Set([...A].filter(x => !B.has(x)));
console.log(difference); // {1, 2}

🧋 类比:

  • 店A独家奶茶:只有A卖的。 (比如A店独有“黑糖波霸”,B店没有)

📊 图中 A 的部分减去交叠部分,就是 A\B。


🌈 第15页:综合理解总结

操作类型含义奶茶店类比图形记忆
并集 (A ∪ B)两店所有口味两家菜单合并两圆全部
交集 (A ∩ B)都卖的口味共同菜单重叠部分
差集 (A - B)A独卖A店独家口味A去掉重叠
子集A包含于BA所有口味都在B店有A完全在B内部

💡 小白速记口诀:

:全都要! :只要重叠! :去掉重复! :小集合躲在大集合里!


🍵 一句话总结(第15页收尾):

集合(Set)是“不重复的奶茶菜单”, 你可以加料(add)、下架(delete)、检查(has)、重做(clear), 还可以和别的店做联合(并集)、PK重叠(交集)、找独家(差集)。

🌳 第16页:树的基本概念

💬 一、什么是“树”?

树(Tree)是一种“层级结构”的数据结构。

它的特点是:

  • 有一个“根节点”(Root)
  • 根节点下面有“子节点”(Child)
  • 每个节点还可以有“孙节点”、“曾孙节点”……

🧋 类比一:奶茶店结构图

奶茶集团总部(根节点)
 ├─ 华东区经理
 │    ├─ 上海分店店长
 │    └─ 杭州分店店长
 └─ 华南区经理
      ├─ 广州分店店长
      └─ 深圳分店店长

这就是一个树形结构。 根节点 = 总部,叶节点 = 各个分店。


🌲 第17页:树的类型

树也分几种类型(就像组织结构不同):

类型特点类比
普通树每个节点能有任意多个孩子总部下属很多分公司
二叉树 (Binary Tree)每个节点最多2个孩子一人最多带两个下属
满二叉树每个节点都有左右两个孩子所有店长都有2个助理
完全二叉树除最后一层外,节点都排满最后一层“差几个”

📘 图例中:

  • a)三叉树:每个节点最多有3个分支。
  • b)普通二叉树:每个节点最多2个分支。

🧋 就像一个总店老板,每个经理手下只能带两家分店。


🧩 第18页:树的遍历(Traversal)

遍历,就是“访问树中所有节点”的顺序方式。

三种经典方式:

  1. 前序遍历(先访问自己)
  2. 中序遍历(自己夹在左右孩子之间)
  3. 后序遍历(最后访问自己)

🍵 生活类比:

你是奶茶集团总裁,要巡视门店。

  • 前序:先看自己总部,再去分店。
  • 中序:先看左边分店 → 自己 → 再看右边分店。
  • 后序:先把所有分店都看完,最后回总部。

🧠 第19页:前序遍历(Preorder Traversal)

📘 规则:

根 → 左 → 右


💻 代码示例:

function preorder(root) {
  if (!root) return;
  console.log(root.val);
  preorder(root.left);
  preorder(root.right);
}

🧋 类比: 1️⃣ 总部先汇报(打印 root) 2️⃣ 左边门店检查 3️⃣ 右边门店检查


📊 执行顺序:

根 → 左 → 右

如果是:

   1
  / \
 2   3
/ \
4  5

输出顺序:1 → 2 → 4 → 5 → 3


🌼 第19页后半:中序遍历(Inorder Traversal)

📘 规则:

左 → 根 → 右


💻 代码示例:

function inorder(root) {
  if (!root) return;
  inorder(root.left);
  console.log(root.val);
  inorder(root.right);
}

🧋 类比: 1️⃣ 先去左边门店 2️⃣ 回来总部汇报 3️⃣ 再去右边门店


📊 执行顺序:

左 → 根 → 右

同样的树:

   1
  / \
 2   3
/ \
4  5

输出顺序:4 → 2 → 5 → 1 → 3


🌿 第20页:后序遍历(Postorder Traversal)

📘 规则:

左 → 右 → 根


💻 代码示例:

function postorder(root) {
  if (!root) return;
  postorder(root.left);
  postorder(root.right);
  console.log(root.val);
}

🧋 类比: 1️⃣ 先视察完左边门店 2️⃣ 再看右边门店 3️⃣ 最后回总部总结汇报。


📊 执行顺序:

左 → 右 → 根

输出顺序:4 → 5 → 2 → 3 → 1


💡 三种遍历方式速记表(第20页总结)

遍历方式顺序奶茶巡视顺序类比输出示例
前序根 → 左 → 右总部先行动1, 2, 4, 5, 3
中序左 → 根 → 右左边先汇报,再总部4, 2, 5, 1, 3
后序左 → 右 → 根最后汇报总部4, 5, 2, 3, 1

🎯 小白记忆口诀:

“前”我先出发, “中”我夹中间, “后”我最后见。


🧋 最后一句话总结(第20页):

树是一种“层级关系”的数据结构, 前序、中序、后序是访问它的三种方式。 就像奶茶集团巡视店面,有三种路线: 先自己、夹中间、最后出场。🌳

🌳 第21页:树的层序遍历(Level Order Traversal)

💬 是什么?

按层级顺序,从上到下、从左到右访问树的节点。

🌰 例子: 一棵树结构如下 👇

    1
   / \
  2   3
 / \   \
4  5    6

层序遍历顺序: 👉 1 → 2 → 3 → 4 → 5 → 6


💡 类比记忆法:

想象你是奶茶集团的巡视员,检查每一层门店:

  • 第1层:先看总部(节点1)
  • 第2层:看分店经理(2、3)
  • 第3层:看店员(4、5、6)

这就叫 按层级顺序检查


💻 代码解释

function levelOrder(root) {
  if (!root) return [];
  let queue = [root]; // 用队列保存待访问节点
  let res = [];

  while (queue.length) {
    let node = queue.shift(); // 取出第一个节点
    res.push(node.val);

    // 把左右孩子依次加入队列
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }
  return res;
}

💡 通俗解释:

就像“排队取号买奶茶”: 1️⃣ 顾客(根节点)最先服务; 2️⃣ 然后下一个(左孩子); 3️⃣ 再下一个(右孩子)。 每服务一个人,就让他的“朋友”(左右孩子)来排队~

📊 口诀:

“先进先出,一层层来” ——这其实就是“队列”思想在树中的应用!


🧩 第22页:树的总结(复盘)

📘 树的遍历方式回顾

遍历方式访问顺序类比
前序根 → 左 → 右老板先讲话
中序左 → 根 → 右左门店汇报后老板讲
后序左 → 右 → 根老板最后发言
层序一层层访问奶茶集团巡视逐层走

🌟 记忆口诀:

“前自己,中夹心,后殿后,层排队”


🔁 第23页:栈与队列的理解

💡 一、什么是“栈”?

栈(Stack)是一种先进后出(LIFO) 的结构。


🧋 生活类比:

想象你在叠奶茶杯 🍵:

  • 最后放上去的杯子,会最先拿出来。
  • 想拿最底下的杯子?必须先拿走上面的所有杯子。

💻 栈的基本操作代码

class Stack {
  constructor() {
    this.items = [];
  }
  push(item) {  // 压入
    this.items.push(item);
  }
  pop() {  // 弹出
    return this.items.pop();
  }
  peek() {  // 查看顶部元素
    return this.items[this.items.length - 1];
  }
  isEmpty() {
    return this.items.length === 0;
  }
}

📘 类比解释:

方法含义奶茶店类比
push加入新杯子把新奶茶放在最上面
pop取出最上层拿出最上面的那杯奶茶
peek看最上层是谁偷看哪杯奶茶在最上面
isEmpty是否空栈奶茶柜空了没?

💡 栈口诀:

“后进先出,上叠奶茶,先取顶层。”


🚶‍♀️ 第24页:队列(Queue)的理解

💬 二、什么是队列?

队列(Queue)是一种先进先出(FIFO) 的结构。


🧋 生活类比:

像顾客排队买奶茶 🍹:

  • 第一个来的顾客最先拿到奶茶。
  • 后面的人得依次等待。

💻 队列的基本实现:

class Queue {
  constructor() {
    this.items = [];
  }
  enqueue(item) {  // 入队
    this.items.push(item);
  }
  dequeue() {  // 出队
    return this.items.shift();
  }
  front() {  // 看队首
    return this.items[0];
  }
  isEmpty() {
    return this.items.length === 0;
  }
}

📘 类比解释:

方法含义奶茶店类比
enqueue加入队尾顾客排队买奶茶
dequeue队首离开最前的顾客拿奶茶走人
front看谁排第一个查看现在轮到谁
isEmpty队伍是否空店门口没人排了吗?

🌟 栈 vs 队列 对比总结(第25页)

特性栈(Stack)队列(Queue)
操作顺序后进先出(LIFO)先进先出(FIFO)
类比叠奶茶杯顾客排队
现实场景浏览器回退、撤销操作打印任务、排队买票
JS 实现push() + pop()push() + shift()

🎯 快速记忆口诀:

栈叠叠,后出先; 队排排,先进先。 栈装奶茶,队接单。 一个叠上去,一个排成行。


📚 小结(第25页)

这几页其实在讲一个“超级常见面试三件套”:

🌳 树结构 + 🔁 栈机制 + 🚶‍♀️ 队列逻辑

🧋 在算法题中,它们经常搭配使用:

  • 模拟“递归”
  • 队列 实现“层序遍历”
  • 管理层级结构

📘 一句话总结:

“树是奶茶集团,栈是叠奶茶杯,队列是排队买奶茶。”

🍹第26页:栈(Stack)与队列(Queue)的应用场景

这页先回顾了:

  • 栈:后进先出(LIFO)
  • 队列:先进先出(FIFO)

🧋 应用 1:栈在生活中的应用

📦 “后进先出”的典型例子:浏览器的“返回”功能。

假设你打开了三个网页:

AB → C

当你点“后退”时,顺序是:

C → BA

最新打开的页面最后出来。 就像叠奶茶杯,最后放上去的最先取出。


💻 栈的代码小例子:

class Stack {
  constructor() {
    this.items = [];
  }
  push(item) {
    this.items.push(item);
  }
  pop() {
    return this.items.pop();
  }
}

📘

  • push():放一个奶茶杯到最上层
  • pop():取最上层那杯

🧠 核心记忆法:

栈 = 奶茶杯叠叠乐 ☕️


🍵 应用 2:队列(Queue)在生活中的应用

📋 “先进先出”的典型例子:打印队列、排队买奶茶。

第一个排队的顾客先拿到奶茶; 后面的人只能等前面的人走。


💻 队列代码小例子:

class Queue {
  constructor() {
    this.items = [];
  }
  enqueue(item) {
    this.items.push(item);
  }
  dequeue() {
    return this.items.shift();
  }
}

📘

  • enqueue():顾客入队
  • dequeue():第一个顾客拿奶茶离开

🧠 口诀:

栈叠叠杯,队排排人。 栈后进先,队先进先。


🔗 第27页:链表(Linked List)的理解

终于到了传说中的“链表”部分 🔥 这可是数据结构面试的常驻嘉宾!


💡 一、什么是链表?

链表是一种“用指针串起来的数据结构”。

它就像一串“奶茶订单单据”,每张单上都写着:

  • 当前奶茶信息(data)
  • 下一张单的编号(next)

📘 结构示意:

[珍珠奶茶 | next][抹茶拿铁 | next][红豆奶茶 | null]

🧋 类比: 每张订单知道“自己”和“下一个订单”, 但不知道前面是谁。


💻 JS 实现结构:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}
  • value:存奶茶信息 🍹
  • next:指向下一个订单单

📊 链表的优点:

✅ 插入、删除快(不需要移动整批数据) ❌ 查找慢(要一个个往后找)

🧋 生活比喻: 数组像“储物柜”——编号清晰; 链表像“纸单串”——要一张张翻。


🧩 第28页:链表的基本操作

链表常见操作有四种: 1️⃣ 遍历 2️⃣ 插入 3️⃣ 删除 4️⃣ 查找


📘 7.2.1 遍历(Traversal)

就是一个个把链表节点“走一遍”。


💻 代码示例:

let current = head;
while (current) {
  console.log(current.value);
  current = current.next;
}

🧋 类比: 就像你拿着第一张订单,一直顺着“next”找下一张,直到走完整条链。


💡 口诀:

“链表像排单,next 指下单。”


🍰 第29页:插入操作

在链表中间插入一个新节点。


📘 图解思路:

AB → C

现在想在 B 和 C 之间插入 D:

👉 新的结构变成:

AB → D → C

💻 代码示例:

let newNode = new Node('D');
newNode.next = targetNode.next; // D指向C
targetNode.next = newNode;      // B指向D

📘 步骤讲解: 1️⃣ 先让 D 指向 C 2️⃣ 再让 B 指向 D ⚡ 顺序不能反,否则链表断掉!

🧋 类比: 像你在排队时插队,要“先抓前面那人,再接后面那人”。 顺序错了,队伍就散了 😂


🧹 第30页:删除操作

删除一个节点(比如删除 B)。


📘 图解:

AB → C

要删 B:

A → C

💻 实现逻辑:

targetNode.next = targetNode.next.next;

📘 解释: 把 B 的上一个节点(A)直接指向 C, 中间的 B 自然“断链”啦。

🧋 类比: “奶茶订单B作废”, 让 A 的 next 改成指向 C,就不再经过B。


🧠 链表总结表(第30页)

操作动作描述奶茶店类比代码关键
遍历走完整条链一张张看订单while(current)
插入插入新节点插队next = next.next
删除删除节点撤销订单next = next.next.next
查找找某个值找订单号if (value == target)

🎯 小白快速记忆口诀:

“链表像单据,一张连一张; 插要接两头,删要断一张。”


🍵 一句话总结(第30页收尾):

链表是一串“有顺序的纸条”, 每张纸知道下一个是谁, 插入删除方便,查找得慢慢翻~

🍰 第31页:堆的理解(Heap)

💡 一、堆是什么?

堆是一种 “特殊的完全二叉树” , 它可以用来快速找到最大值或最小值。


📘 重点理解:

  • “完全二叉树” → 每一层从左到右都排满(像排整齐的奶茶杯架)。

  • 每个节点都满足“堆性质”:

    • 大顶堆(Max Heap) :父节点 ≥ 子节点。
    • 小顶堆(Min Heap) :父节点 ≤ 子节点。

🧋 生活类比:

想象一个奶茶销量排行榜:

名次销量
🥇 珍珠奶茶999
🥈 奶盖绿茶880
🥉 芋圆奶茶700
  • 排在最上面的(父节点)是销量最高的那杯。
  • 如果销量下降,就要“掉下去”;
  • 新奶茶爆火,就要“往上冒”。

这整个结构,就是一个堆(Heap)


🌳 第32页:堆的两种类型

📊 图示说明:

✅ 大顶堆(Max Heap):

每个父节点都比子节点大。

       9
     /   \
    6     7
   / \   / \
  4  5  3  2

🧋 类比:销量排行榜 → 顶端是“销量最高的奶茶”。


✅ 小顶堆(Min Heap):

每个父节点都比子节点小。

       1
     /   \
    2     3
   / \   / \
  4  5  6  7

🧋 类比:价格排行榜 → 顶端是“最便宜的奶茶”。


💡 一句话记忆:

大顶堆:最大值在顶 小顶堆:最小值在顶


🧩 第33页:堆的常见操作

主要有三个核心动作:

操作含义奶茶店类比
插入(Insert)新元素入堆新奶茶上榜
删除(Delete)删除堆顶最热销奶茶被下架
堆化(Heapify)维持堆结构排行榜重新调整顺序

💻 JS 实现代码(精讲版):

class MaxHeap {
  constructor() {
    this.heap = [];
  }

  // 获取父子节点索引
  getParentIndex(i) { return Math.floor((i - 1) / 2); }
  getLeftIndex(i) { return i * 2 + 1; }
  getRightIndex(i) { return i * 2 + 2; }

  // 插入元素
  insert(value) {
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1);
  }

  // 上浮操作
  shiftUp(index) {
    while (index > 0) {
      let parent = this.getParentIndex(index);
      if (this.heap[parent] < this.heap[index]) {
        [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]];
        index = parent;
      } else break;
    }
  }
}

💡 通俗解释:

“堆顶是最强的,插入时往上冒!”

就像: 1️⃣ 你推出一款新奶茶; 2️⃣ 如果它销量比上面的高,就往上爬; 3️⃣ 一直爬到不再比别人高为止。

📘 这个过程就是 “上浮(Shift Up)”


🍵 第34页:堆删除与堆化

💻 删除堆顶(下架最热门)

extractMax() {
  if (this.heap.length === 0) return null;
  if (this.heap.length === 1) return this.heap.pop();

  let max = this.heap[0];
  this.heap[0] = this.heap.pop(); // 用最后一个元素顶上去
  this.shiftDown(0);
  return max;
}

shiftDown(index) {
  let left = this.getLeftIndex(index);
  let right = this.getRightIndex(index);
  let largest = index;

  if (this.heap[left] > this.heap[largest]) largest = left;
  if (this.heap[right] > this.heap[largest]) largest = right;

  if (largest !== index) {
    [this.heap[index], this.heap[largest]] = [this.heap[largest], this.heap[index]];
    this.shiftDown(largest);
  }
}

🧋 类比:

有奶茶下架(删除堆顶), 最下面的一杯临时补上来。 然后重新比较排名, 该下去的下去,该上去的上去。

这就是 “下沉(Shift Down)”


💡 一句话总结:

插入:上浮 🔼 删除:下沉 🔽 堆化:上下调整保持平衡 ⚖️


🌟 第35页:堆的应用场景

场景示例类比
优先队列(Priority Queue)操作系统任务调度谁优先级高谁先执行
Top K 问题找出前 K 大或前 K 小的元素最畅销的奶茶前 3 名
排序算法(堆排序)Heap Sort按销量从高到低排列菜单
实时流数据动态更新排行榜抖音热搜榜实时更新

💡 小白快速记忆口诀:

“堆是排行榜,插上浮,删下沉。” “大顶选冠军,小顶找便宜。” “Top K、优先队列,都离不开它!”


🧋 最后总结(第35页收尾):

堆是一棵“会自动排序的树”。 它能帮我们快速找到最大值或最小值。 插入时往上冒,删除时往下沉。 是算法界的“自动化排行榜机器”!

每天选出“销量 Top1 奶茶”, 放入榜单,更新销量后再选下一天的冠军。


💎 第39页:堆总结 & 引入新主题「图结构」

📘 这页收尾提到:

  • 堆能高效找出最大/最小值;
  • 常用于排序、TopK、优先队列;
  • 时间复杂度:O(n log n)。

然后——我们迎来了重磅新角色:


🕸️ 第40页:图(Graph)的理解


💡 一、什么是“图”?

图是一种“节点 + 连接关系”的数据结构。 用来表示各种“关系网络”。


🧋 举例:

  • 地图中的城市和道路 🏙️(城市 = 节点,道路 = 边)
  • 奶茶外卖配送路线 🚴‍♀️(门店 = 节点,道路 = 边)
  • 社交网络(人 = 节点,好友关系 = 边)

📘 二、图的表示方法

主要两种表示法:

表示方式图示特点
邻接矩阵(Adjacency Matrix)✅ 用二维数组表示是否相连结构清晰,但占空间
邻接表(Adjacency List)✅ 用链表或数组记录邻居节省空间,更灵活

💻 邻接矩阵举例:

假设有 5 个节点:A、B、C、D、E 用 0/1 表示是否相连 👇

  A B C D E
A 0 1 1 0 0
B 1 0 0 1 1
C 1 0 0 1 0
D 0 1 1 0 1
E 0 1 0 1 0

🧋 类比:

奶茶配送中心互联图。 “1” 表示两家门店之间有配送路线, “0” 表示暂时没开通外送。


💻 邻接表举例:

A[B, C]
B[A, D, E]
C → [A, D]
D → [B, C, E]
E → [B, D]

🧋 类比:

每家奶茶店只记录“我能直达的店”名单。 比如 A 店可以直达 B、C;D 店能直达 B、C、E。


🧠 图的类型补充

类型说明生活类比
有向图边有方向外卖骑手只能单向送货(A→B)
无向图边双向两家门店互相配送
带权图边有权重每条路有“距离”或“成本”
无权图没标权重只记录是否连通

💡 小白快速记忆口诀:

“点连点,线成图;有向单行,无向双行。” “矩阵看全景,列表更省心。”


🎯 总结(第40页收尾)

概念解释奶茶店比喻
节点+连接关系门店与配送路线
邻接矩阵二维表路网总览图
邻接表链表集合每店的直达路线
有向/无向图单向/双向外卖单行道 vs 双向道

🌟 一句话记忆:

图是“关系的集合”, 奶茶店之间的“外卖路线”就是一张图。

🌍 第41~43页:图的遍历(Graph Traversal)


💡 一、什么叫“图的遍历”?

图遍历就是“走遍每个节点,不重复访问”。

📘 你可以把它想象成外卖骑手送奶茶:

  • 每个店铺/顾客是一个节点。
  • 每条路是连接的“边”。
  • 你要规划路线,确保所有店都送到,不重复跑。

🚴‍♀️ 9.2.1 深度优先遍历(DFS)

(Depth First Search)


📘 定义:

“一条路走到黑”,直到走不动再返回。

🧋 生活类比: 就像外卖骑手送奶茶, 你先选一家最近的门店, 再从这家继续往下送,直到没有新顾客了, 再往回退找别的路线。


💻 代码解释:

function dfs(node, visited = new Set()) {
  if (visited.has(node)) return; // 防止重复访问
  visited.add(node);
  console.log(node); // 打印访问的节点
  for (let neighbor of graph[node]) {
    dfs(neighbor, visited); // 递归继续深入
  }
}

📘 通俗讲: 1️⃣ 访问当前节点 2️⃣ 深入它的邻居节点 3️⃣ 一直深入到没有邻居 4️⃣ 再“回溯”到上一级

🧋 比喻:

外卖员“钻小巷”,送完一家再去它邻居那儿,直到送不动,再往回走。


✅ DFS 访问顺序例子:

假设图是这样👇

0 → 1 → 3  
 ↓   ↘︎ 2

输出顺序可能是:0 → 1 → 3 → 2 因为是一路深潜。


💡 小白记忆口诀:

DFS:深挖到底,送完回头。 就像“一个骑手一条线跑到尽头再回来”。


🚗 9.2.2 广度优先遍历(BFS)

(Breadth First Search)


📘 定义:

一圈一圈往外扩散地访问节点。

🧋 生活类比: 像外卖调度中心派单。 先派骑手送最近的几个顾客(第一层), 再送稍远的(第二层), 再送最远的(第三层)。


💻 代码解释:

function bfs(start) {
  let queue = [start];
  let visited = new Set([start]);

  while (queue.length) {
    let node = queue.shift(); // 出队
    console.log(node);
    for (let neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        queue.push(neighbor);
        visited.add(neighbor);
      }
    }
  }
}

📘 通俗解释: 1️⃣ 先访问起点; 2️⃣ 把它的所有邻居都放进“排队名单”; 3️⃣ 按顺序出队访问。

🧋 比喻:

BFS 就像奶茶店的“广播派单系统”📣: 先处理周围顾客,再慢慢往外扩。


✅ BFS 访问顺序例子:

假设图是:

0 → 1 → 2  
↓   ↓
3   4

遍历顺序:0 → 1 → 3 → 2 → 4 一圈一圈往外送~


💡 小白记忆口诀:

BFS:一层层送,DFS:一条线送。 BFS 是“层序访问”,DFS 是“递归深入”。


🧠 DFS vs BFS 总结表(第43页)

特性DFSBFS
思路一条路走到黑一圈圈往外扩
数据结构栈(Stack)或递归队列(Queue)
类比外卖员一路送到底派单系统层层广播
优点实现简单,节省空间找最短路径很快
缺点可能绕远路队列可能太大

🧋 一句话总结(第43页收尾):

DFS 是“探险家”,BFS 是“调度员”。 前者钻到底,后者一圈圈。


🔢 第44~45页:排序算法(Sorting Algorithms)


💡 10.1 什么是排序算法?

排序算法的目标是让一组数据按“从小到大”或“从大到小”排列。

🧋 奶茶店例子: 把今日销量从高到低排一下榜单。📊 这时候就得选一个排序方法—— “你想快点排完?还是省资源?”


🍵 10.2 常见的排序算法

最常见的有以下几种(第45页开始举例):

算法思想复杂度奶茶店比喻
冒泡排序相邻比较,交换顺序O(n²)两两比销量,泡泡往上浮
选择排序每次选最小/最大放前面O(n²)选出当天最畅销奶茶放榜首
插入排序把新元素插入已排序部分O(n²)新奶茶插入榜单
快速排序分治法+递归O(n log n)分区间挑中间值
归并排序拆分再合并O(n log n)把不同区域的销量表合并

💻 冒泡排序代码讲解(第45页重点)

function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换位置
      }
    }
  }
  return arr;
}

📘 逻辑: 1️⃣ 相邻两个比较; 2️⃣ 大的往后挪(像泡泡上浮); 3️⃣ 每一轮确定一个最大值。

🧋 奶茶店比喻:

每个奶茶销量两两PK,输的往后, 最后“冠军奶茶”浮到榜单顶端!💥


💡 冒泡排序口诀:

“相邻比,大的滚; 一趟趟,排成行。”


🎯 小白记忆总结(第45页收尾)

分类思想类比
DFS一条路走到底外卖员钻巷子
BFS一圈圈往外送调度中心广播派单
冒泡排序两两比较浮上去奶茶销量比拼
选择排序每次选最小放前面榜单选冠军
插入排序插入到已排序部分新奶茶上榜

🌈 一句话总结:

图的遍历帮你“走遍所有店”; 排序算法帮你“排好所有奶茶”。 BFS、DFS 找路线;冒泡、选择排顺序~🍹

🧊 第46页:插入排序(Insertion Sort)


💡 一、思想

“从无序部分拿出一个,插入到有序部分合适的位置。”

🧋 生活类比: 就像奶茶店在更新“销量榜”:

  • 你已经排好前几名;
  • 新出的奶茶要插进合适的位置;
  • 把比它销量低的往后挪一点。

💻 代码理解:

function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    let key = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j]; // 往后挪
      j--;
    }
    arr[j + 1] = key; // 插入正确位置
  }
  return arr;
}

📘 举例说明: 原始数组 [5, 3, 4, 1] 1️⃣ 第1个5默认已排好 2️⃣ 插入3 → [3, 5, 4, 1] 3️⃣ 插入4 → [3, 4, 5, 1] 4️⃣ 插入1 → [1, 3, 4, 5]


🧠 小白口诀:

“一杯杯插入,排好前队;小的往前,大的靠后。”


🍵 第47页:快速排序(Quick Sort)


💡 一、思想:

“分而治之”:选一个基准,把比它小的放左边,比它大的放右边,然后递归处理两边。

🧋 生活类比: 像奶茶店选销量中位数的奶茶当“分界线”:

  • 销量低的放左边;
  • 高的放右边;
  • 每一边再继续细分。

💻 代码讲解:

function quickSort(arr) {
  if (arr.length <= 1) return arr;
  let pivot = arr[Math.floor(arr.length / 2)];
  let left = arr.filter(x => x < pivot);
  let right = arr.filter(x => x > pivot);
  let equal = arr.filter(x => x === pivot);
  return [...quickSort(left), ...equal, ...quickSort(right)];
}

📘 举例: 数组 [4, 6, 2, 7, 1] 基准 pivot = 2 → 左边 [1],右边 [4, 6, 7] 再对右边递归排序 → [1, 2, 4, 6, 7]


🧠 小白口诀:

“快排快在分区细,左右分开递归洗。” “像奶茶销量榜——中位线分两边,继续分!”


🍰 第48页:归并排序(Merge Sort)


💡 一、思想:

“先拆再合”——先把数组拆成最小单元(一个个杯子),再逐步合并成有序队列。

🧋 奶茶店比喻:

把全国销量榜拆成各城市榜单; 每个榜单排好后,再合并成总榜单。


💻 代码讲解:

function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left, right) {
  const res = [];
  while (left.length && right.length) {
    if (left[0] < right[0]) res.push(left.shift());
    else res.push(right.shift());
  }
  return [...res, ...left, ...right];
}

📘 思路: 1️⃣ 拆成单个元素; 2️⃣ 两两排序合并; 3️⃣ 最终拼成完整排序。


🧠 小白口诀:

“归并拆拆拆,再合合合。” “像奶茶店各地榜单合成全国榜。”


📊 第49页:排序算法复杂度对比表

这页是重头戏!🎯 它比较了常见排序算法的性能。

算法平均复杂度最好最坏稳定性内存占用适用场景
冒泡O(n²)O(n)O(n²)✅ 稳定原地数据量少
选择O(n²)O(n²)O(n²)❌ 不稳定原地小规模排序
插入O(n²)O(n)O(n²)✅ 稳定原地基本有序时快
快速O(n log n)O(n log n)O(n²)❌ 不稳定原地大量数据最常用
归并O(n log n)O(n log n)O(n log n)✅ 稳定占空间超大规模数据

🧋 奶茶店例子总结:

算法类比
冒泡奶茶两两比销量,泡泡上浮
选择每轮选冠军放前面
插入新奶茶插入榜单
快速分区中位法选拔
归并分城市合并榜单

💡 一句话记忆(第49页收尾):

“冒泡慢,快排强;插入稳,归并忙。 数据小用冒泡,数据大用快排, 要稳定找归并,要节省选插入。”


🧮 第50页:冒泡排序复盘


💬 一、是什么?

冒泡排序是最基础的“相邻比较交换法”,每一轮把最大的数“冒”到最后。


💻 代码回顾:

function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

🧋 奶茶店类比:

每两杯奶茶比销量,输的往后排, 一轮结束后,“销量王”浮到榜首。


🌈 适用场景:

  • 数据量少;
  • 已经接近有序;
  • 教学演示算法基础时最常用。

🎯 最终记忆表(第50页总结)

排序算法思想奶茶店类比快慢
冒泡相邻比较泡泡上浮
选择每轮选最小选冠军
插入插入到前面插队上榜
快排分区递归中位数分堆
归并拆分合并分店汇总

🧠 小白终极口诀:

冒泡比相邻,插入插队行; 选择选冠军,快排左右拼; 归并先拆后,再合才成群。


🍵 一句话总结(第50页):

排序算法就像奶茶排行榜的不同排法, 有的“逐个PK”(冒泡), 有的“挑冠军”(选择), 有的“插榜单”(插入), 有的“区域赛分治”(快排), 有的“城市榜合并”(归并)。

🧋 第51页:冒泡排序(Bubble Sort)运行原理


💡 一、是什么?

冒泡排序是一种“两两比较、交换位置”的算法,像泡泡一样把最大的值慢慢“浮到最上面”。


📊 图解理解:

在图上我们看到数组每轮比较:

起始状态:[12, 35, 99, 18, 76]
第一趟结束:[12, 35, 18, 76, 99]

👉 最大的99已经“浮”到了最后一位。 下一趟再排 [12, 35, 18, 76],直到所有数字都有序。


🧋 类比奶茶店:

假设今天统计奶茶销量:

  • 每两杯奶茶 PK 一下;
  • 谁销量更高就“往后坐一位”;
  • 一趟下来,“销量冠军”就自动浮到最后一位。

再一轮一轮比,直到全部排好。


💡 一句话口诀:

“相邻比,大的滚,一趟定一尾。”


💻 第52页:冒泡排序代码讲解

function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换两个元素
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

📘 逻辑拆解: 1️⃣ 外层循环控制轮数(每趟确定一个最大值); 2️⃣ 内层循环负责“相邻比较”; 3️⃣ 如果前面比后面大 → 交换位置。


🧋 举例: 初始 [5, 1, 3] → 比 5 和 1 → 交换 → [1, 5, 3] → 比 5 和 3 → 交换 → [1, 3, 5] 结束 ✅


⚙️ 优化思路(第52页下半)

加入一个布尔变量 flag

如果某一趟中没有发生交换,说明已经有序,可以提前结束循环。

let flag = false;
for (...) {
  ...
  if (交换了) flag = true;
  if (!flag) break;
}

🧋 比喻:

奶茶榜单每次都要重排吗?不用。 如果销量顺序已经稳定,就不用再PK啦~


🧠 第53页:冒泡排序的优缺点总结

优点缺点
实现简单效率低,O(n²)
稳定(不打乱相等元素)需要频繁交换
适合小数据集数据多时性能差

🧋 类比:

冒泡法像手动给奶茶排行榜挨个对比, 虽然笨但保稳。小店 OK,大连锁太慢。


🔍 第54页:二分查找(Binary Search)

这部分是重点中的重点 🚨,一定要理解!


💡 一、什么是二分查找?

在“有序数组”中,通过每次取中间值,不断缩小范围,直到找到目标。


🧋 生活类比: 想象你在菜单上找“抹茶拿铁”:

  • 菜单已经按字母排序(A~Z);
  • 你先看中间页:是“奶盖绿茶”; → 说明“抹茶拿铁”应该在右半边。
  • 再取右半页中间:是“芋圆奶茶”; → 还在左半边!
  • 这样一分一分,很快就找到目标啦。

📊 图解(第54页图中箭头):

数组 [1, 3, 4, 6, 7, 8, 10, 13, 14] 要找 6

1️⃣ 中点是 7 → 6 比 7 小,去左边; 2️⃣ 左边中点是 4 → 6 比 4 大,去右边; 3️⃣ 找到 6 🎯


💻 第55页:二分查找代码实现

function binarySearch(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;       // 找到
    else if (arr[mid] < target) left = mid + 1; // 去右边
    else right = mid - 1;                       // 去左边
  }
  return -1; // 没找到
}

📘 步骤讲解: 1️⃣ mid = (left + right) / 2 找中间; 2️⃣ 如果正好是目标 → 返回下标; 3️⃣ 如果目标更大 → 查右半部分; 4️⃣ 如果目标更小 → 查左半部分。

🧋 奶茶店比喻:

你在菜单册找“芋圆奶茶”, 每次翻中间那页——如果没找到,就只看一半。 一次次缩范围,效率超高。


💡 一句话记忆:

“有序才二分,中点定方向。” ——没排序的数组是没法二分的!


🎯 小白快速对比总结(第55页收尾)

算法功能特点类比
冒泡排序排序稳定但慢奶茶两两PK,销量浮上去
二分查找搜索必须有序菜单折半查找奶茶

🧠 终极口诀:

冒泡排队慢悠悠, 二分找奶茶翻半书。 前者排序靠交换, 后者查找靠中点。


🌈 一句话总结(第55页总结):

冒泡帮你“排好顺序”, 二分帮你“快速查找”。 一个是整理榜单的店长,一个是找口味的顾客~ 🍹

🧋 第56页:二分查找完整代码复盘


💻 代码逻辑(完整版)

function binarySearch(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;       // 找到了目标
    else if (arr[mid] < target) left = mid + 1; // 去右边
    else right = mid - 1;                      // 去左边
  }
  return -1; // 没找到
}

📘 思路回顾:

  • 数组必须是有序的!
  • 每次取中间值 mid,和目标 target 比较。
  • 决定往哪半边继续查找(不断“折半”)。

🧋 比喻:

菜单上有100款奶茶,你想找“抹茶拿铁”。 翻开第50页,不是,就判断是A-M的区段还是N-Z。 每次折一半,一下子就找到了~


⚙️ 第57页:进阶版二分查找 —— 旋转数组(Rotated Array)


💡 背景

普通二分查找要求“整体有序”。 但如果数组被旋转过怎么办?🤔

例如:

原数组: [0,1,2,4,5,6,7]
旋转后: [4,5,6,7,0,1,2]

这时你想找 0,传统二分就懵了。


📊 图解说明

这一页图中出现了“分界点 Point”,数组在这里“断开重接”。 左边 [4,5,6,7] 是递增的; 右边 [0,1,2] 也是递增的; 只是拼接在一起后“不整体有序”。


💻 代码实现

function search(nums, target) {
  let left = 0, right = nums.length - 1;
  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (nums[mid] === target) return mid;

    // 判断左半边是否有序
    if (nums[left] <= nums[mid]) {
      if (nums[left] <= target && target < nums[mid]) {
        right = mid - 1;  // 目标在左边
      } else {
        left = mid + 1;   // 否则去右边
      }
    }
    // 右半边有序
    else {
      if (nums[mid] < target && target <= nums[right]) {
        left = mid + 1;   // 目标在右边
      } else {
        right = mid - 1;  // 否则去左边
      }
    }
  }
  return -1;
}

🧋 生活比喻:

奶茶菜单分两部分打印, 左边是“经典奶茶”页,右边是“水果奶茶”页, 但打印装订时顺序错了(右边的页先放前面)。

要找到“珍珠奶茶”,就要先判断当前页属于哪一类菜单, 再决定往前翻还是往后翻。


💡 记忆口诀:

“先判哪边有序,再看目标在不在。”


🧭 第58页:二分查找的延伸场景


除了旋转数组,还能用二分解决这些:

  • 🧱 查找边界(第一个大于某值)
  • 🧊 寻找最小值位置
  • ⚡ 找峰值(比邻居都大)

这些应用都离不开同一核心:

“有序区间 + 折半查找 + 区间缩小”。

🧋 类比:

你在奶茶机的温度控制器里想找“合适温度区间”, 每次测中间温度,然后决定调高还是调低。


⚡ 第59页:快速排序(Quick Sort)正式登场!


💡 一、是什么?

快速排序是一种基于“分治思想(Divide and Conquer)”的高效算法。


📘 核心步骤:

1️⃣ 选一个基准元素(pivot); 2️⃣ 所有比它小的放左边,比它大的放右边; 3️⃣ 对两边递归执行同样操作。


💻 代码实现:

function quickSort(arr) {
  if (arr.length <= 1) return arr;

  let pivot = arr[Math.floor(arr.length / 2)];
  let left = arr.filter(x => x < pivot);
  let right = arr.filter(x => x > pivot);
  let equal = arr.filter(x => x === pivot);

  return [...quickSort(left), ...equal, ...quickSort(right)];
}

🧋 奶茶店类比:

店长要给所有奶茶按销量排序: 先随手选一杯奶茶(pivot)当“分界线”; 把销量比它高的放右边,比它低的放左边; 然后再对两边继续用同样方法分组。

最终就得到完整的销量排行榜!


💡 第60页:快排总结 + 时间复杂度分析


操作平均时间复杂度最坏情况稳定性
快速排序O(n log n)O(n²)❌ 不稳定

🧠 关键点:

  • 平均性能非常优秀;
  • 但当数组本身有序时,退化为最坏情况;
  • 可以通过“随机选pivot”优化。

🧋 生活比喻:

快速排序就像“比赛选拔”: 店长选一个奶茶销量作为中位线, 把比它高的和低的分两边再继续比较。

分得越平均,速度越快!


💡 小白记忆口诀:

“快排快在分两边,左小右大递归连; 分治思想记心间,平均 n log n,最坏 n 平方。”


🌈 总结(第60页收尾)

算法核心思想类比记忆特点
二分查找折半定位翻菜单找奶茶快速查找
旋转二分判断有序区再折半菜单页乱序找饮品稍复杂
快速排序分区递归奶茶销量左右分组排序之王

一句话总结:

二分查找是“快速找奶茶”, 快速排序是“高效排奶茶榜”。 两者都是“聪明地分区缩小范围”, 一个找位置,一个排顺序~💡🍵

🌈 第61~63页:快速排序(Quick Sort)复盘与复杂度分析


💡 一、快速排序的核心思想(复习巩固)

快排 = “分而治之(Divide and Conquer) ” 简单讲就是:

先分区,再排序。

📘 操作三步走: 1️⃣ 选一个基准值(pivot); 2️⃣ 小的放左,大的放右; 3️⃣ 递归处理左右两边。


💻 代码实现(第62页)

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left >= right) return;

  let pivot = arr[left]; // 选最左边为基准
  let i = left, j = right;

  while (i < j) {
    while (i < j && arr[j] >= pivot) j--;
    while (i < j && arr[i] <= pivot) i++;

    if (i < j) [arr[i], arr[j]] = [arr[j], arr[i]];
  }

  [arr[left], arr[i]] = [arr[i], arr[left]]; // 基准归位

  quickSort(arr, left, i - 1);
  quickSort(arr, i + 1, right);
  return arr;
}

📘 通俗解释:

  • pivot 像一条“中间线”;
  • 左右两边的人(数字)按大小分好队;
  • 递归让每个小队自己再排一次。

🧋 奶茶店比喻:

店长选出销量中等的“红豆奶茶”当中线: 卖得更好的放右边(高销量榜); 卖得少的放左边(低销量榜)。 然后再分别细排两边~


⚙️ 时间复杂度推导(第63页)

书上这段看起来很学术(T(n) = 2T(n/2) + n), 其实我们用生活案例来拆👇:

把 n 杯奶茶分成两组(各 n/2 杯),每次分组都要比一遍(O(n) 次比较)。

这样递归地分、比、排:

T(n) = 2T(n/2) + n

继续展开:

T(n) = 4T(n/4) + 2n
T(n) = 8T(n/8) + 3n
...

当递归到底时(每组只剩一杯奶茶),得到:

T(n) = n log n

所以时间复杂度是:

✅ 平均情况:O(n log n) ❌ 最坏情况(几乎有序时):O(n²)


🧋 奶茶店类比:

每次选中“销量中位数”做分组,是理想情况(分得均匀)→ 排得快。

但如果每次都选到销量最少的那杯当 pivot(比如每次都选错人 😅)→ 退化成 O(n²)。


💡 快排的优势:

✅ 平均速度最快 ✅ 代码简洁 ✅ 内存占用少(原地排序)

缺点: ❌ 不稳定(相同元素可能被交换) ❌ 最坏情况会慢


🧠 小白记忆口诀:

“快排快在分两边,分治思想记心间; 左右平均笑开颜,不平均就哭一篇。” 😂


🧊 第64~65页:选择排序(Selection Sort)


💡 一、是什么?

每一轮从未排序区间中找出最小值,放到当前“已排序区间”的末尾。


🧋 奶茶店例子:

店长要排出“最冷门奶茶排行榜”: 每次从所有奶茶中找出销量最低的那杯,放到榜单前面。 然后再在剩下的中继续找最低的……直到排完。


📘 举例(第65页图)

初始数组:

[12, 56, 80, 91, 20]

第1轮:

  • 找最小 → 12
  • 它已经在最前,不动 → [12, 56, 80, 91, 20]

第2轮:

  • 剩余 [56, 80, 91, 20]
  • 最小是 20,和 56 交换 → [12, 20, 80, 91, 56]

第3轮:

  • 最小是 56,与 80 交换 → [12, 20, 56, 91, 80]

第4轮:

  • 最小是 80,与 91 交换 → [12, 20, 56, 80, 91] ✅ 排完

💻 代码实现:

function selectionSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j; // 记录最小值位置
      }
    }
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  return arr;
}

📘 步骤拆解: 1️⃣ 假设第一个是最小; 2️⃣ 遍历后面找更小的; 3️⃣ 如果找到更小的,就和当前位置交换。


💡 时间复杂度:

平均、最好、最坏都是 O(n²)。

因为每一轮都要扫描整个未排序区间。


🧋 奶茶店类比:

店长每天都要从几十种奶茶里找“最少卖的那杯”, 一轮轮找完要很久。

但优点是稳定、安全,不会出错。


⚙️ 优缺点总结:

优点缺点
实现简单效率较低
占用内存少需要大量比较
适合小规模排序不稳定

🧠 小白口诀:

“选择选最小,一轮定一位; 找完换上前,次次要排对。”


🌈 第65页收尾对比总结:

算法思想时间复杂度稳定性类比记忆
快速排序分区 + 递归O(n log n)奶茶销量分区榜
选择排序每轮找最小O(n²)店长选冷门奶茶

一句话总结:

快排是“聪明分区型”,效率高; 选择是“老派挨个找”,稳但慢。

前者像算法界的“销售经理”, 后者像“老店长”,做事慢但靠谱~😆

💡 第66页:插入排序的核心思想(是什么)


🧠 一句话概括:

“拿到新元素,把它插进前面已经排好序的部分里。”


📘 举例说明: 假设我们要对 [3, 1, 7, 5, 2] 排序。

最开始,默认“第一个数”3 已经是一个有序区。 然后:

  • 把 1 拿出来 → 插到 3 前面。
  • 把 7 拿出来 → 插到合适位置(3 后面)。
  • 把 5 拿出来 → 插进 3 和 7 之间。
  • 以此类推。

🧋 生活类比:

想象你在买奶茶排队 🍹, 队伍前面的人已经按“点单速度”排好了, 每来一个新顾客,就要插进“比自己慢的人前面、比自己快的人后面”。

一次次插队,最后整队人自动排好。


📊 图解讲解(第67页)

初始状态:
有序区:[3]
无序区:[1, 7, 5, 2, 4]

➡ 插入 1 → [1, 3, 7, 5, 2, 4] ➡ 插入 7 → [1, 3, 7, 5, 2, 4] ➡ 插入 5 → [1, 3, 5, 7, 2, 4] ➡ 插入 2 → [1, 2, 3, 5, 7, 4] ➡ 插入 4 → [1, 2, 3, 4, 5, 7]


📘 过程分析:

轮次当前元素插入位置结果
11插到3前面[1,3,7,5,2,4]
27已在正确位置[1,3,7,5,2,4]
35插到3后面、7前面[1,3,5,7,2,4]
42插到1后面、3前面[1,2,3,5,7,4]
54插到3后面、5前面[1,2,3,4,5,7]

🧮 第68页:算法实现


💻 代码实现:

function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    let current = arr[i];
    let j = i - 1;

    // 从后往前比较,找到插入位置
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j]; // 往后移
      j--;
    }

    arr[j + 1] = current; // 插入到正确位置
  }

  return arr;
}

📘 通俗解释: 1️⃣ current 是当前要插的奶茶(元素)。 2️⃣ 从后往前扫描“已排好队的人”(有序区)。 3️⃣ 只要前面的人比你慢(值大)→ 把他们往后挪一步。 4️⃣ 当遇到比你快的,就在他后面插队。


🧋 类比:

一群顾客已经在排队点单。 你手拿奶茶菜单走进队伍, 看着前面一个个顾客—— 谁点得比你慢,你就礼貌地插到他前面 😆。


⚙️ 第69页:插入排序的过程图解


这页的图展示了“插入”三次的动画演变👇:

1️⃣ 插入4:[1, 3, 5, 7, 2, 4, 9, 6] 2️⃣ 插入9:[1, 3, 4, 5, 7, 9, 2, 6] 3️⃣ 插入6:[1, 3, 4, 5, 6, 7, 9, 2]


📘 核心逻辑没变,只是每一步都在:

  • 拿出当前值
  • 移动前面比它大的元素
  • 把它插进正确位置

🧋 小比喻:

插入排序就像你慢慢排奶茶单: 每来一个新单,插入到正确的销售排行榜位置。


💎 第70页:插入排序总结与应用场景


⚙️ 时间复杂度:

情况复杂度
最好(几乎有序)O(n)
平均O(n²)
最坏(完全逆序)O(n²)

🧋 所以它在“小数据量 + 基本有序” 的情况下非常高效。


✅ 优点:

  • 实现简单;
  • 稳定排序;
  • 适合部分有序的场景。

❌ 缺点:

  • 数据量大时效率低;
  • 移动次数多。

📘 应用场景:

1️⃣ 数量少(例如几十个以内); 2️⃣ 数据基本有序; 3️⃣ 可用作更复杂算法的“优化阶段”(比如希尔排序)。


🧋 奶茶店类比总结:

场景排序算法特点
所有奶茶销量随机快速排序分区快、效率高
每天销量差不多插入排序稳定插队、轻调整
要选冷门款选择排序一轮轮选最小

💡 小白记忆口诀:

“插入靠挪位,左边保有序; 一步步插队,稳定又省事。”


🌈 一句话总结(第70页):

插入排序就像顾客排队买奶茶—— 每次来一个新顾客,就在前面合适位置插队, 最后队伍自然有序。

💡 第71页:分而治之 vs 动态规划 —— 核心思想


一、什么是“分而治之”(Divide and Conquer)

📘 定义:

把一个大问题分成若干个小问题,分别解决后再合并结果。

🧋 奶茶店类比:

店长要算出全国奶茶总销量。 不可能一个人算完所有门店,于是:

  • 先按城市分组;
  • 每个城市统计自己总销量;
  • 最后汇总出全国数据。

这就是 “分而治之” :先拆后合。


💻 代码示例(第71页)

function mergeSort(arr) {
  if (arr.length <= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

📘 逻辑讲解: 1️⃣ 不停把数组“切一半”; 2️⃣ 直到只剩一个元素; 3️⃣ 再一对一合并排序好的小数组。

🧋 类比:

就像奶茶店开分店,每个分店先内部排好销量, 然后再汇总成全国总榜~


⚙️ 第72页:动态规划(Dynamic Programming)


一、什么是动态规划?

📘 定义:

“把复杂问题拆成子问题,通过记忆之前的结果避免重复计算。”

🧋 奶茶店例子:

你在算「不同原料组合能做出多少种奶茶」。 如果每次都重新算,太浪费! 所以你记下: “加珍珠+红茶”能出3种, “加奶盖+乌龙”能出2种……

之后用的时候直接查表,不再重算。

这就叫 动态规划


💡 区别总结(第72页下半)

对比项分而治之动态规划
思想拆解问题再合并结果拆解+记忆结果
是否重叠子问题❌ 否✅ 有
是否存储中间结果❌ 不存✅ 存(数组或表)
举例快速排序、归并排序背包问题、斐波那契数列

🧋 打个比方:

分而治之像“分店独立算完汇总”, 动态规划像“总部记账防止重复计算”。


💻 第73页:动态规划小例子(斐波那契)

function fib(n, memo = {}) {
  if (n <= 1) return n;
  if (memo[n]) return memo[n];

  memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
  return memo[n];
}

📘 通俗解释:

  • 递归计算 F(n) = F(n-1) + F(n-2);
  • 但为了不重复计算, 用 memo(记忆表)保存中间结果。

🧋 类比:

像奶茶店记下“原料组合A能出几种奶茶”, 下次再算到A时直接查账,不用重算~


💡 小白口诀:

“分而治之先拆后合; 动态规划记得存档。”


🧠 第74页:动态规划应用场景

📘 常见题型: 1️⃣ 背包问题(Knapsack) → 在有限容量下挑选最优商品。

🧋 类比:

奶茶车出摊,每天只能带 10 杯, 要怎么组合口味,让利润最高?

2️⃣ 最短路径问题(Dijkstra/Floyd) → 求最省时间的送货路线。

🧋 类比:

外卖小哥要从门店出发送到6个顾客, 哪条路最短、时间最少?

3️⃣ 股票买卖最大收益 → 找出买卖时机的最大差价。

🧋 类比:

店长想知道哪天进奶、哪天卖出能赚最多 💰。


✨ 一句话记忆:

分而治之解决“能不能”; 动态规划解决“最优解”。


🍰 第75页:归并排序(Merge Sort)


💡 一、是什么?

一种“分而治之”的典型排序算法。


📘 思想: 1️⃣ 把数组不断一分为二; 2️⃣ 分到只剩一个元素; 3️⃣ 再一对一合并成有序数组。


🧋 类比:

奶茶店总部统计销量: 先让各分店内部排好销量榜, 然后两两合并出全国排行榜。


📊 图示讲解:

图中展示了 [38, 27, 43, 3, 9, 82, 10] 的归并过程 👇

1️⃣ 拆成单个:[38]、[27]、[43]、[3]、[9]、[82]、[10] 2️⃣ 左右两两合并:

[27,38] [3,43] [9,82] [10]

3️⃣ 再次合并:

[3,27,38,43,9,10,82]

4️⃣ 最终排序完:

[3,9,10,27,38,43,82]

💻 代码实现(递归写法)

function mergeSort(arr) {
  if (arr.length <= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  while (left.length && right.length) {
    if (left[0] < right[0]) result.push(left.shift());
    else result.push(right.shift());
  }
  return [...result, ...left, ...right];
}

📘 通俗解释:

  • 不停拆(divide);
  • 每次合并(conquer);
  • 合并时保持左右顺序不乱。

🧋 奶茶店比喻:

把全国门店销量表分成小店 → 小区 → 城市 → 全国, 每次都合并两个有序榜单, 直到得到全国大榜。


⚙️ 性能分析:

场景复杂度
最好O(n log n)
最坏O(n log n)
稳定性✅ 稳定

🧋 优点:

稳定、高效、适合处理超大数据集。

缺点:

需要额外内存存放中间数组。


💡 小白口诀:

“归并先拆后合成, 排序两边小合成。 拆到最细不再分, 合并有序才叫真。”


✨ 第75页总结

概念思想奶茶店比喻特点
分而治之拆小问题再合分店汇总销量不保存中间结果
动态规划拆+记忆优化记账防重复计算保存结果避免重复
归并排序分而治之应用各分店销量合并稳定高效

💡 一句话总结:

分而治之像“开分店拆任务”; 动态规划像“财务记账防浪费”; 归并排序像“分店排行榜合成全国榜单”。

💎 第76~78页:归并排序(Merge Sort)


一、算法思想(复习+实现细节)

📘 定义:

归并排序采用 分而治之思想(Divide and Conquer) : 把一个大数组不断拆分成两半 → 分别排序 → 再合并成一个有序数组。

🧋 类比:

就像奶茶总部要整合全国销量榜:

  • 每个城市先内部排好销量;
  • 再两两城市合并;
  • 直到合成全国总榜单。

💻 核心代码(第76页)

function mergeSort(arr) {
  if (arr.length <= 1) return arr; // 递归终止条件:单个元素就是有序的

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid)); // 左半排序
  const right = mergeSort(arr.slice(mid));   // 右半排序

  return merge(left, right);
}

// 合并两个有序数组
function merge(left, right) {
  let result = [];
  let i = 0, j = 0;

  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) result.push(left[i++]);
    else result.push(right[j++]);
  }

  // 拼上剩余部分
  return result.concat(left.slice(i)).concat(right.slice(j));
}

📘 通俗解释:

1️⃣ 拆:

  • 不停地把数组“对半切”;
  • 切到只剩一个元素时停止。 2️⃣ 合:
  • 把左右两个有序的小数组,像“拉拉队合并队伍”那样一边比一边合并。

🧋 奶茶店类比:

各分店先排好销量榜(左、右)→ 总部把两个分店榜单合并成一个总榜。

比如: 左榜:[3, 8, 15] 右榜:[5, 10, 18]

合并过程:3 vs 5 → 3 小先上 → 接着 5、8、10、15、18……


💡 时间复杂度分析(第77页)

  • 拆的层数:log n
  • 每层合并:O(n) → 总体:O(n log n) 稳定排序(不会打乱相同元素顺序)

📈 空间复杂度:O(n)(需要临时数组)

🧋 小结:

“分得多、合得快,效率稳定但占空间。”


⚙️ 应用场景(第78页)

  • 适合大规模排序(数据量百万级)
  • 外部排序(文件太大放不进内存)
  • 数据流排序(流式处理)

🧋 奶茶店例子:

总部要合并全国几千家门店销量榜单, 快排太乱、选择太慢, 归并就是“批量分组 + 稳定合并”的最佳方案 ✅。


💡 第79~80页:贪心算法(Greedy Algorithm) & 回溯算法(Backtracking)


一、贪心算法(Greedy)

📘 定义:

每一步都做“当下最优选择”,希望最后得到全局最优结果。

🧋 奶茶店比喻:

店长派你去“抢购原料”, 每次都优先选「折扣最大」的供货商。

虽然每次选得都很好,但不一定整体最优(可能漏掉组合折扣)。


💻 示例(第79页):

👉 求最少硬币组成某个金额:

function coinChange(coins, amount) {
  coins.sort((a, b) => b - a); // 先按面值从大到小排序
  let count = 0;
  for (let coin of coins) {
    while (amount >= coin) {
      amount -= coin;
      count++;
    }
  }
  return amount === 0 ? count : -1;
}

📘 思路:

  • 先用最大的硬币;
  • 剩下的再用次大的;
  • 一直换,直到凑齐或凑不齐。

🧋 奶茶店生活版:

老板让你用最少数量的优惠券买 100 杯奶茶: 你先用 50 元券、再用 20 元、再 10 元…… 每次都贪最划算的那张~

但有时组合更优(比如 20+20+10 可能比 50 更好), 所以贪心不保证“全局最优”,只是“局部最优”。


⚙️ 应用场景:

  • 找零钱、活动分配;
  • 最小生成树(Kruskal、Prim);
  • 霍夫曼编码(压缩算法)。

💡 小白口诀:

“眼前最优先抓住,未来是否最优看命数。” 😂


🧩 二、回溯算法(Backtracking)

📘 定义:

“枚举所有可能,一条路走不通就回头换条路。”

🧋 奶茶店比喻:

你要设计新品奶茶组合:

  • 先试:红茶 + 珍珠 + 奶盖 ✅
  • 再试:红茶 + 椰果 + 奶盖 ✅
  • 试到 “红茶 + 咖啡 + 芋圆” 🤢 → 不行,撤退,换别的!

这就是“走错路→退一步→换选项”。


💻 代码示例(第80页)

function backtrack(path, choices) {
  if (path 满足条件) {
    记录结果;
    return;
  }

  for (let choice of choices) {
    做选择(choice);
    backtrack(path + choice, 剩余选择);
    撤销选择(choice);
  }
}

📘 通俗版: 1️⃣ 尝试一个可能(做选择) 2️⃣ 继续向下探索(递归) 3️⃣ 如果失败(不满足条件)→ 回退一步(撤销选择)


🧋 奶茶店案例:

就像你在试新菜单, 每次组合一个新口味, 试过不行就撤回原材料重新搭。

直到找到完美搭配(最优解)~✨


⚙️ 应用场景:

  • 全排列 / 组合问题;
  • 数独;
  • 八皇后问题;
  • 迷宫寻路;
  • 路径搜索。

💡 小白口诀:

“能走就走,走错回头; 试到合适,收获全优。”


✨ 第80页总结表:

算法思想奶茶店类比特点
归并排序分治分店销量合并稳定高效
贪心算法局部最优优惠券每次选最大简单但不一定全优
回溯算法穷举试探调奶茶口味全试一遍找所有解但慢

💡 一句话通关总结:

🌈 归并像“总部合并榜单”; 💰 贪心像“每次捡最大优惠”; 🔄 回溯像“试口味、撤回、再试”;

三者合起来,就是算法界的—— 「奶茶连锁管理智慧三件套」🧋👑

🍰 第81页:回溯算法代码讲解


💻 代码部分(全排列问题 permute)

var permute = function(nums) {
  const res = [], path = [];
  backtracking(nums, nums.length, []);
  return res;

  function backtracking(n, k, used) {
    if (path.length === k) {
      res.push(Array.from(path));
      return;
    }

    for (let i = 0; i < k; i++) {
      if (used[i]) continue;
      path.push(n[i]);     // 做选择
      used[i] = true;      // 标记已用
      backtracking(n, k, used); // 递归
      path.pop();          // 撤销选择
      used[i] = false;     // 恢复状态
    }
  }
}

🧠 通俗解释:

这个算法解决的问题是——

“给定几个元素(比如 [1,2,3]),生成它们所有的排列顺序。”

例如结果为:

[1,2,3]
[1,3,2]
[2,1,3]
[2,3,1]
[3,1,2]
[3,2,1]

🧋 生活版比喻:

假设你是奶茶店的 店长助理, 要帮老板把三种配料的组合(珍珠、椰果、奶盖)都试一遍。

你会这样做👇

1️⃣ 先选珍珠(做选择) 2️⃣ 接着选椰果,再选奶盖(继续尝试) 3️⃣ 做完一杯,记下来(结果入表) 4️⃣ 再回到上一步,换掉最后一种配料(撤销选择) 5️⃣ 继续尝试所有组合(继续递归)

等试完全部配料组合后,你就得到了「所有可能的奶茶菜单」✨

这就是 回溯算法(Backtracking)

“尝试—回退—再尝试,直到所有组合都走一遍。”


🧩 口诀帮助记忆:

“选一个、试一条,走错了就回老家; 每走一条新路径,都留下一杯奶茶。” 😂


💡 第82页:算法三巨头的总结对比


这一页是整章的关键「脑图总结」💥 帮你从全局看懂算法的“思维模式”。


一、分而治之(Divide and Conquer)

📘 思想:

把大问题拆成小问题 → 分别解决 → 再合并结果。

🧋 奶茶店类比:

总部统计全国奶茶销量。 先让各地分店算出自己的销量(子问题), 再汇总成全国报告(合并结果)。

📦 常见题目:

  • 归并排序
  • 快速排序
  • 最大子数组和
  • 逆序对问题

📘 特征口诀:

“能分能合就是它。”


二、动态规划(Dynamic Programming)

📘 思想:

把问题拆分后,保存中间结果,避免重复计算。 从底层往上推,找最优解。

🧋 奶茶店类比:

你要算“不同配料组合能做出多少杯奶茶”。

上次算过“珍珠+红茶 = 3种”, 那就记下来,下次直接查账! 不用重算。

📦 常见题目:

  • 背包问题
  • 最长公共子序列(LCS)
  • 股票买卖收益
  • 最短路径

📘 口诀:

“记账防重算,存表求最优。”


三、贪心算法(Greedy Algorithm)

📘 思想:

每一步都选当前最优的方案,希望整体也最优。

🧋 奶茶店类比:

老板发优惠券,你每次都选折扣最大的那张! 虽然贪心,但简单有效。

📦 常见题目:

  • 活动安排问题(选择最多活动)
  • 最小生成树(Kruskal、Prim)
  • 零钱兑换(用最少硬币)

📘 口诀:

“眼前最香的先拿,整体靠运气。”


四、回溯算法(Backtracking)

📘 思想:

枚举所有可能,一条路走不通就回头换条路。

🧋 奶茶店类比:

研发新口味奶茶: 珍珠→奶盖→椰果(不行)→撤销→加波霸→再试~

📦 常见题目:

  • 全排列
  • 组合求和
  • 数独
  • 八皇后

📘 口诀:

“能走就走,错了就撤。”


🧭 图解对照总结(第82页底部)

表格总结超级实用👇:

思维路线特点常见应用
分而治之拆分 → 求子问题 → 合并结果快排、归并、二分搜索
动态规划拆分 + 存结果 → 自底向上背包、最长子序列
贪心算法局部最优一步步做活动安排、最短路径
回溯算法全试一遍 + 回退数独、排列组合

🧋 生活一图记忆法:

想象你是「奶茶连锁总部AI」🤖:

算法你在干嘛类比场景
分而治之拆分城市销量表再合并全国销量分析
动态规划记住计算过的配料组合避免重复试配方
贪心算法每次选最大优惠优惠券策略
回溯算法试遍所有新口味新品研发测试

🧠 一句话终极记忆:

“分治拆问题,动规防重算, 贪心图省事,回溯全穷举。”


✨ 你学完这页之后,其实已经掌握了「算法思维的四大核心模式」。 以后遇到新题,先问自己:

“这个问题我能不能拆(分治)?” “有没有重复子问题(动规)?” “能不能一步步贪最优(贪心)?” “要不要全试一遍(回溯)?”

这就是所有算法的底层逻辑!