🧠 第1页:数据结构的理解(是什么、有哪几类、区别)
💬 1️⃣ 什么是数据结构?
数据结构 = “数据 + 关系 + 操作” 简单讲:它是“组织和管理数据的方式” 。
📦 举个例子: 想象你是个奶茶店老板,要保存顾客的订单。
- 用 数组 就像你有一叠排好序的订单本(第1个、第2个、第3个)。
- 用 链表 就像你用回形针把订单一张一张串起来(中间插新订单不影响整体顺序)。
- 用 栈 像叠奶茶杯(后放的先拿:后进先出)。
- 用 队列 像排队买奶茶(先来的先买:先进先出)。
- 用 树 像店长组织层级(店长→主管→员工)。
- 用 图 像外卖配送地图(门店和骑手之间是“路线连接”)。
🗂️ 第2页:常见的数据结构类型
这页讲了几种常见的数据结构:
| 类型 | 特点 | 小白生活类比 |
|---|---|---|
| 数组 Array | 连续存储、下标访问快、插删慢 | 一排储物柜(编号取物快,换位置麻烦) |
| 链表 Linked List | 不连续、插删快、查找慢 | 串珠子(插珠子容易,但数第几个要一个个找) |
| 栈 Stack | 后进先出(LIFO) | 一摞碗(最后放的最先拿) |
| 队列 Queue | 先进先出(FIFO) | 排队买票(先来先买) |
| 树 Tree | 层级关系 | 家谱结构或公司组织架构 |
| 图 Graph | 任意连接关系 | 地铁线路图、社交网络 |
| 哈希表 Hash Table | 键值对存储,查找快 | 电话簿(查名字立刻得号码) |
🔍 第3页:数据结构的区别(考点重点)
数组 vs 链表
- 数组:查找快(可直接按编号取),插入删慢。
- 链表:插入删快(随便插珠子),查找慢(得一个个找)。
栈 vs 队列
- 栈:后进先出 → 像洗碗机,最上面的碗先洗。
- 队列:先进先出 → 像排队取奶茶。
树 vs 图
- 树:层级明确(只能一条路径)。
- 图:可以多条路径交叉(像地铁线互通)。
🌟 小口诀助记:
数快查,链快插; 栈叠叠,队排排; 树分支,图乱连。
🧩 第4页:算法的理解(是什么、特性)
💬 什么是算法?
算法 = 解决问题的一套明确步骤。
🧋 举例: 你点奶茶 →
- 接单
- 加冰/不加冰
- 调料混合
- 摇一摇
- 打包
这就是一个“算法流程”。
📘 定义里强调三点:
- 输入(原料)
- 输出(结果)
- 有穷性(不能无限摇啊)
💡 算法的五大特性
| 特性 | 解释 | 生活类比 |
|---|---|---|
| 输入性 | 有输入 | 顾客要点单 |
| 输出性 | 有输出 | 做出奶茶 |
| 有穷性 | 步骤有限 | 不会永远摇 |
| 确定性 | 每步明确 | 不同店员结果一样 |
| 可行性 | 能实现 | 材料和设备都够用 |
🧮 第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包含于B | A所有口味都在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)
遍历,就是“访问树中所有节点”的顺序方式。
三种经典方式:
- 前序遍历(先访问自己)
- 中序遍历(自己夹在左右孩子之间)
- 后序遍历(最后访问自己)
🍵 生活类比:
你是奶茶集团总裁,要巡视门店。
- 前序:先看自己总部,再去分店。
- 中序:先看左边分店 → 自己 → 再看右边分店。
- 后序:先把所有分店都看完,最后回总部。
🧠 第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:栈在生活中的应用
📦 “后进先出”的典型例子:浏览器的“返回”功能。
假设你打开了三个网页:
A → B → C
当你点“后退”时,顺序是:
C → B → A
最新打开的页面最后出来。 就像叠奶茶杯,最后放上去的最先取出。
💻 栈的代码小例子:
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页:插入操作
在链表中间插入一个新节点。
📘 图解思路:
A → B → C
现在想在 B 和 C 之间插入 D:
👉 新的结构变成:
A → B → 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)。
📘 图解:
A → B → 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页)
| 特性 | DFS | BFS |
|---|---|---|
| 思路 | 一条路走到黑 | 一圈圈往外扩 |
| 数据结构 | 栈(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] ✅
📘 过程分析:
| 轮次 | 当前元素 | 插入位置 | 结果 |
|---|---|---|---|
| 1 | 1 | 插到3前面 | [1,3,7,5,2,4] |
| 2 | 7 | 已在正确位置 | [1,3,7,5,2,4] |
| 3 | 5 | 插到3后面、7前面 | [1,3,5,7,2,4] |
| 4 | 2 | 插到1后面、3前面 | [1,2,3,5,7,4] |
| 5 | 4 | 插到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」🤖:
| 算法 | 你在干嘛 | 类比场景 |
|---|---|---|
| 分而治之 | 拆分城市销量表再合并 | 全国销量分析 |
| 动态规划 | 记住计算过的配料组合 | 避免重复试配方 |
| 贪心算法 | 每次选最大优惠 | 优惠券策略 |
| 回溯算法 | 试遍所有新口味 | 新品研发测试 |
🧠 一句话终极记忆:
“分治拆问题,动规防重算, 贪心图省事,回溯全穷举。”
✨ 你学完这页之后,其实已经掌握了「算法思维的四大核心模式」。 以后遇到新题,先问自己:
“这个问题我能不能拆(分治)?” “有没有重复子问题(动规)?” “能不能一步步贪最优(贪心)?” “要不要全试一遍(回溯)?”
这就是所有算法的底层逻辑!