先承认一个事实:集合与映射这一对概念,大多数人是在高中数学课上最后一次认真接触它们。当时的印象多半停留在文氏图和“y=f(x)”这种符号游戏上,考完就还给了老师。但如果在真实开发、数据分析和系统设计里摸爬滚打得够久,你迟早会发现——这两个词其实是工程师的底层暗语。数据库表连接是映射,缓存命中是映射,接口鉴权是集合运算,订单去重也是集合去重。你学过的“集合与映射”不是数学课本上的僵尸概念,而是理解从一行代码到一套系统如何运转的通用语法。
这篇文章我想换个角度来聊这两个概念:不打算按教科书顺序讲定义、定理、证明,而是把它们放到真实工程问题的场景里重新拆一遍。你会看到集合与映射如何化身成 Python 里的 set 和 dict,如何长成数据库里的表和索引,如何隐藏在 RESTful API 和权限模型里。不管你是刚接触编程的新手,还是写过几年业务代码的开发,都可以从里面找到一些值得反复琢磨的东西。
1. 集合:表面上在讲数学,实际上在讲数据的“边界”
1.1 集合的三个基本性质,以及它们变成的工程准则
集合论里对集合有三个基础要求:确定性、互异性和无序性。这三个词看起来特别像概念填空,你仔细想想,它们几乎是现代数据处理系统的默认行为准则。
确定性指的是一个元素是否属于某个集合,结果必须明确,不能模棱两可。对应到工程上,就是所有数据结构都需要明确定义“相等”的规则。你在 set 里放入两个 1,它们必然被视作同一个元素;你在数据库里给用户表加了主键,就不会允许两行记录拥有相同的主键。这背后都是在落实“确定性”这件事:每一个元素面对集合时只有一个确定的身份。
互异性更直白,集合里不允许重复元素。但工程上“去重”不仅是“把重复的删掉”,它还牵涉到“以哪个版本为准”“重复数据里隐藏的业务冲突怎么处理”。比如你从多个数据源拉取用户信息,同一个用户可能出现三次,但三次的姓名、手机号不一样。这时候你做的不是简单去重,而是要从数据冲突里决定保留策略。这个问题的本质是:集合的互异性约束是对的,但是“谁才是这个集合里应该保留的那一个”需要你自己定义。
无序性往往是最容易被忽视、也最容易在工程里引发诡异 Bug 的那一条。数学上的集合没有顺序,{1, 2, 3} 和 {3, 1, 2} 是同一个集合。但工程实现里,集合的遍历顺序取决于底层存储的实现方式。Python 的 set 看起来“好像有点顺序”,实际上这个顺序是基于哈希值分布算出来的,连程序员自己都不能依赖它。你要是把一个 set 转成 list 再去按位置取元素,分分钟就是事故现场。
1.2 把集合操作翻译成实战场景
集合有四种基本运算:交集、并集、差集、对称差集。很多人学到这里就结束了,其实这四种运算在业务里的出现频率高得吓人。
我举个具体例子。假设你在运营一个内容社区,手里有两张表:一张表是“已注册用户”,另一张表是“最近 7 天活跃用户”。老板要一份“注册了但最近 7 天没登录的用户名单”,也就是“沉默用户”。用 SQL 写,一个 NOT IN 或者 LEFT JOIN ... WHERE b.id IS NULL 就出来了;用 Python 模拟,就是 registered_users - active_users 这个差集运算。
再来一个需要交集的场景。假设你搞了一个拉新活动,奖励规则是“同时满足以下条件:年满 18 岁、完成实名认证、有至少一笔订单”。这三个条件筛选出来的用户集合分别是 A、B、C,最终获奖名单就是 A ∩ B ∩ C。
你可能会觉得,这些都不需要专门学集合论也能写出来。但关键在于:如果你脑子里没有“集合”这个抽象模型,碰到复杂一点的场景就容易把代码写得一团糟——用 for 循环嵌套,每层循环里再写 if 判断;而一旦你意识到自己是在做集合运算,一行 set(a) & set(b) 或者一句 INNER JOIN 就解决了。抽象能力的作用不是让你背公式,而是让你更快认出问题的本质结构。
1.3 集合不是“高级列表”:无序性带来的坑
很多初学者会把 set 当成“自动去重版 list”,这个直觉方向没错,但落到具体行为上会踩坑。
看这段 Python 代码:
python复制a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7}
print(a - b) # 差集,输出 {1, 2}
print(a & b) # 交集,输出 {3, 4, 5}
print(a | b) # 并集,输出 {1, 2, 3, 4, 5, 6, 7}
print(a ^ b) # 对称差集:输出 {1, 2, 6, 7}
这里的 &、|、-、^ 四个运算符分别对应了交集、并集、差集、对称差集。熟悉了这套写法之后,很多原本需要写循环加标记数组的逻辑,几行就能写完。
但有个细节必须留神:set 是可迭代的,但它不保证顺序。如果你需要“保持插入顺序的去重”,在 Python 3.7+ 里正确选择是 dict(它的键有插入顺序),而不是 set。如果你用 set 做了去重然后发现输出顺序每次都不一样,别怀疑是随机数问题,先去想想是不是集合本身的天然无序性导致的。
python复制original = ["apple", "banana", "apple", "cherry", "banana"]
# 用 set 去重,结果顺序不确定
result_set = list(set(original))
print(result_set) # 可能输出 ['cherry', 'banana', 'apple']
# 保持原始顺序的去重方式
result_ordered = list(dict.fromkeys(original))
print(result_ordered) # 输出 ['apple', 'banana', 'cherry']
因为 dict 键天然保持插入顺序,以“键存在即跳过”的逻辑去重后,再取键列表就既能去重又能保留顺序。这两种写法的区别并不难理解,但掉进“用 set 去重然后依赖顺序”这个坑的人,我在 code review 里见过不止一次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 映射:从函数到字典,工程世界的最小连接件
2.1 映射不是“一一对应”:单射、满射、双射的工程对应物
“映射”这个概念在数学里比“函数”更宽泛——函数要求每个输入对应唯一输出,映射也同样要求“定义域里的每个元素都有确定像”,但并未要求不同输入不能对应同一输出。这里面拆出来的单射、满射、双射,在工程实践里全都有明确对应物。
单射,即“不同的输入不会映射到同一输出”。这在系统设计里直接对应唯一键约束。
最典型的是数据库主键。一个合法的用户 ID,绝不能同时代表两个用户。在分布式系统里,如何生成全局唯一的单射,是雪花算法、UUID、号段模式这些方案要解决的核心问题。单射保证了“如果你看到了这个输出,你能反推出唯一的输入来源”,这是大量关联查询成立的前提。
满射,即“每个输出值至少被某个输入映射到”。这个性质在数学定义里很重要,但在工程里往往不是天然成立,需要刻意设计。举个例子:你设计了一个状态机,订单状态集合是 {待支付, 已支付, 已发货, 已完成, 已取消},从“当前状态 + 操作事件”到“新状态”的映射表里,如果某种状态永远不可能被触达,那这个状态要么是后备方案(比如未来业务才启用),要么就是你的状态迁移表里存在永远无法走通的分支——这种死状态在设计评审时是应该被挑出来的。
双射,就是既单射又满射,两个集合之间存在“一一对应”。工程上严格的双射场景其实不多,但“相互转换”是大家都熟悉的。比如将一个业务 ID 编码成短链接,或者将整数 ID 映射到随机字符串邀请码,如果编码算法能做到可逆,那它就是双射。做数据迁移时,新旧表结构里同一实体的 ID 映射,也应尽量追求双射,否则一旦出现两个新 ID 指向同一个旧 ID,数据就拉不回去了。
2.2 哈希表:让映射在常数时间内工作
说完了映射的抽象性质,来看它最著名的工程实现——哈希表。Python 里的 dict、Java 里的 HashMap、Redis 里的哈希结构,本质都是一回事:让映射查找平均在 O(1) 内完成。
哈希表的设计思路很巧妙:要给一个键找到对应的值,不是挨个去列表里找,而是对键算一个哈希值,再用这个哈希值定位到存储桶。这个过程可以把一个“查找”问题降级成“计算 + 定位”问题,大大缩短响应时间。
但哈希表并不是没有代价。哈希冲突是绕不开的:即使哈希函数设计得再好,不同键也可能映射到同一个桶。业界处理冲突的方案主要有两种:链地址法(每个桶挂一条链表或红黑树)和开放地址法(冲突了就往下一个空位放)。Java 8 之后的 HashMap 在桶内元素超过阈值时会把链表转成红黑树,就是为了防止恶意构造的键把哈希表退化成链表,导致查找时间从 O(1) 恶化成 O(n)。
用 Python 验证一下普通列表和字典的查找速度差异:
python复制import time
import random
size = 1_000_000
data_list = list(range(size))
data_dict = {i: i for i in range(size)}
indices = random.sample(range(size), 10000)
start = time.time()
for i in indices:
# 列表的线性查找
_ = i in data_list
print("list in 耗时:", time.time() - start)
start = time.time()
for i in indices:
# 字典的哈希查找
_ = i in data_dict
print("dict in 耗时:", time.time() - start)
这个例子非常直观:当数据量到百万级时,列表的 in 操作线性扫描和字典的哈希查找已经有肉眼可见的差距;等到千万级,列表基本没法用。理解了这个原理,你就知道为什么很多系统要做缓存、要建索引——本质上都是在为“映射查询”提供一个更快的物理实现。
2.3 从数组下标到聚簇索引:映射的多种物理形态
映射思想在工程里的落地形态远不止哈希表一种。数组下标其实就是一个最原始的映射:下标 -> 元素。因为内存是连续排布的,这个映射天然是 O(1) 的。这也是为什么“能用数组就用数组”,它几乎是所有数据结构里访问速度最快的。
数据库的聚簇索引也很有意思。MySQL InnoDB 的表结构本身是一棵以主键为键值的 B+ 树,数据行直接挂在叶子节点上,主键查到哪个叶子节点,数据就在那里。也就是说,主键到数据行这个映射被做成了物理存储布局的一部分。这时候查询“按主键找一行”就不需要二次回表,速度自然快。
再看 Redis 里的 ZSET(有序集合),它里面同时维护了“成员 -> 分数”的哈希映射和“分数 -> 成员列表”的跳表结构,两种映射用一套 API 组合起来,实现了排序、区间查询、排名估算这些能力。你能说 ZSET 是集合还是映射?它俩都是。当你在设计一个功能时,能同时从"集合"和"映射"两个视角出发,思路会清晰很多。
3. 用代码把集合与映射落下来:从 set/dict 到底层原理
3.1 dict/set 底层的哈希原理与复杂度对比
有了前面的铺垫,再看 Python 里的 dict 和 set 就会觉得它们其实是一对孪生兄弟:set 可以理解为“只有键没有值”的 dict,底层都依赖哈希表。操作 dict 的键和操作 set 的元素,时间复杂度规律也基本一致。
日常最多用到的操作大致有这么几类:
| 操作 | dict 示例 | set 示例 | 平均时间复杂度 |
|---|---|---|---|
| 插入 | d[k] = v |
s.add(x) |
O(1) |
| 删除 | del d[k] |
s.remove(x) |
O(1) |
| 查找 | k in d |
x in s |
O(1) |
| 遍历 | for k in d |
for x in s |
O(n) |
| 交集/并集 | 无直接对应 | s1 & s2 / s1 | s2 |
O(min(len(s1), len(s2))) |
需要特别注意,这里的 O(1) 是平均情况,不是最坏情况。如果哈希函数设计极差,或者攻击者故意构造大量哈希值相同的键,哈希表可能退化到 O(n)。Python 从 3.3 版本开始给字符串哈希加入了随机盐,就是为了在某种程度上缓解这类哈希碰撞攻击的风险。这个细节也说明,安全设计不是只有业务代码层面才有,一个基础数据结构里都可能埋着安全考量。
3.2 自定义对象如何正确实现哈希与相等
当一个项目越做越大,你会不可避免地把自定义类放进集合或者作为字典的键。这时候有一个最容易出的错:只重写 __eq__,不重写 __hash__。
Python 对哈希有一个硬性约束:两个对象如果相等(a == b),它们的哈希值必须相等(hash(a) == hash(b))。如果你只定义 __eq__,Python 会自动把 __hash__ 设为 None,这时候对象就变成不可哈希的,放进 set 或当 dict 键会直接抛 TypeError。
反过来的坑是:你实现了 __hash__,但它是基于可变字段算的。对象作为键放进字典之后,你改了某个字段,导致它的哈希值变了——然后你再去字典里查这个键,大概率查不到,因为字典在插入时用的是旧的哈希值定位的桶,现在这个键在你眼里是同一个对象,但在哈希表的结构里它已经“搬家”了。
正确做法是:用不可变字段参与哈希计算。比如订单对象,用订单号来算哈希和判断相等,订单的其他字段随便改,都不会破坏它在字典和集合里的身份。
python复制class Order:
def __init__(self, order_id: str, amount: float):
self.order_id = order_id
self.amount = amount
def __hash__(self):
return hash(self.order_id)
def __eq__(self, other):
if isinstance(other, Order):
return self.order_id == other.order_id
return False
3.3 业务里集合算子的实战组合
写业务代码时,很多时候不是单一集合操作能解决问题,而是需要组合。我举一个我做过的数据对账场景。
当时在做一个支付系统的每日对账,需要比对本地订单表和第三方支付平台返回的交易记录。两条数据源有各自的订单号,对账的目标是找出“本地有但平台没有”以及“平台有但本地没有”的单子。当时我先用集合装载双方订单号,然后一步算出结果:
python复制local_ids = set(local_order["order_id"] for local_order in local_orders)
platform_ids = set(platform_record["transaction_id"] for platform_record in platform_records)
# 本地有、平台没有
only_local = local_ids - platform_ids
# 平台有、本地没有
only_platform = platform_ids - local_ids
# 两边都有,可以继续比对金额、状态等字段
common_ids = local_ids & platform_ids
总共三行就完成了对账逻辑的核心部分。如果不用集合,用嵌套循环去比对,几万条数据跑下来性能就很难看了。而这套代码的可读性也比一大段 for + if 好太多——看到 - 就知道是“差集”,看到 & 就知道是“交集”,意图一目了然。
4. 数据库与 SQL:集合与映射的工业级实践
4.1 表是集合,行是元素:为什么 SQL 叫“关系模型”
关系数据库这个“关系”英文是 Relation,学名就叫“关系模型”。这个模型的理论基础正是集合论:一张表可以看作一个集合,一行记录是这个集合里的一个元素。
这个视角一旦建立,很多 SQL 写法就变得通顺了。SELECT 是在做“从这个集合里筛出符合条件的元素构成新集合”,UNION 是在做集合并集,INTERSECT 是交集,EXCEPT(或者 MySQL 里用 NOT IN 替代)是差集。为什么会有一大堆 SQL 优化器相关的学问?因为优化器本质上是在回答同一个问题:如何最快地从一个大集合里算出一个子集或者多个集合的联结结果。
有一个特别能体现集合思维的经典案例:找出“所有课程都选了的学生”。如果走过程化思路,会想:对每个学生遍历课程,然后统计数量。但集合思维先把“学生选课”这个关系看成一张选课表,然后思考:要判断某个学生是否选了所有课程,等价于“课程集合”是不是“该学生所选课程集合”的子集,或者等价于“全部课程集合减去该学生所选课程集合”是否为空集。
用 SQL 写出来通常是 NOT EXISTS 或者带 GROUP BY 加 HAVING COUNT 的方式。当你看到这些写法的内核是一个集合包含关系的判断时,就能理解 SQL 为什么这样设计了。
4.2 JOIN 与主外键:映射在数据库中的具象化
如果说表是集合,那么表与表之间的 JOIN 关系就是映射的工业级具象。
一个最典型的例子:用户表和订单表。订单表里通常有一个 user_id 字段,它指向用户表里的主键。这个 user_id 建立的关系正是一个映射:user_id -> user。当我们 JOIN 用户表和订单表时,本质上是在按订单的 user_id 把每一笔订单映射回对应的用户记录,再对两条记录做横向拼接。
关联表的出现则把“多对多”映射变成了现实。博客系统的文章和标签,一篇文章可以贴多个标签,一个标签下面可以有多篇文章,这就是个典型的多对多关系。你没法用两个普通外键表达,于是引入中间表 article_tags(article_id, tag_id),每一行都表达“某篇文章映射到某个标签”,中间表作为一个联结集合,把多对多拆成了两个一对多。
理解了这层映射关系,你对数据库设计的很多困惑都会消失。比如:外键到底要不要建索引?答案是如果你经常按外键查询,就该建。因为外键本身是一个映射的起点,频繁用它去关联另一张表,没有索引就是全表扫描,性能会非常差。
4.3 索引的映射本质,以及一个典型的索引失效案例
索引为什么能加速查询?因为它在“被索引字段”和“表记录位置”之间建立了一个有序映射。没有索引时,数据库只能做全表扫描,相当于在数组里用线性查找;有了索引,数据库可以先在 B+ 树(本质上是一棵有序跳表)里按索引键快速定位,这就是 O(log n) 级别的映射查找。
但索引不是万能的。“对索引列使用函数”是特别经典的失效场景:
sql复制SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
这里 create_time 建了索引,但 DATE(create_time) 这个函数包裹导致索引无法直接按区间扫描,数据库只能把每一行都计算一次 DATE(create_time),再与目标日期比较。索引失效的原因说白了就是:索引里存的是原始时间值,但查询条件要的是函数的计算结果,映射对不上了。
改成下面这种写法就能用上索引:
sql复制SELECT * FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00';
这不是什么高深的技巧,但搞懂了背后的映射原理,你以后看到类似写法就能自己推理出为什么该这么改写,而不是死记“索引列别套函数”这个结论。
5. 集合与映射思维下的 API 与权限设计
5.1 用户-角色-权限模型:一场多对多映射的实战
权限系统是几乎所有后台系统都绕不开的模块,而经典的 RBAC(基于角色的访问控制)模型,本质上就是两层映射的叠加。
第一层映射是“用户 -> 角色”,第二层映射是“角色 -> 权限”。用户到底能干什么,就是这两个映射复合后的结果:用户是某些角色的集合,每个角色拥有一个权限集合,用户最终拥有的权限就是这些角色权限集合的并集。
用 Python 来表示这个计算过程会特别直观:
python复制# 用户拥有的角色集合
user_roles = {"admin", "editor"}
# 每个角色拥有的权限集合
role_permissions = {
"admin": {"post.create", "post.delete", "user.manage"},
"editor": {"post.create", "post.edit"},
}
# 用户最终权限 = 各角色权限集合的并集
permissions = set()
for role in user_roles:
permissions |= role_permissions.get(role, set())
print(permissions)
# {'post.create', 'post.delete', 'user.manage', 'post.edit'}
在数据库里,这个模型通常表达成用户表、角色表、权限表,以及用户-角色关联表、角色-权限关联表。看到没有?每个关联表都是一个映射的载体。脑子里没有集合与映射这套模型的话,去理解权限框架的源码会特别吃力;有这套模型,一眼就看穿整个结构。
5.2 接口鉴权与数据权限中的集合思维
接口鉴权里有一个高频操作:判断某个用户是否有某个权限。这个“是否有”的判断就是一个集合包含关系:required_permission in user_permissions。如果用户权限是一个大列表,每次都遍历判断,性能差不说,代码还啰嗦;把它转成 set 之后,一次哈希查找就完事。
数据权限比功能权限复杂得多。比如一个管理系统里,区域经理只能看自己辖区内的数据,总部管理员能看全部数据。这时候你通常需要查出“用户可见的数据 ID 集合”,再用这个集合去限制查询范围。有人会写出 WHERE region_id IN (SELECT id FROM regions WHERE manager_id = ?) 这种查询,这本质上就是先计算一个“可见区域集合”,再判断目标数据的 region_id 是否属于这个集合。想明白这是集合运算后,优化思路就多了:可以先在应用层把集合算好,再拼参数;也可以让数据库通过索引直接完成。
5.3 信息架构里的“归类”与“检索”其实都是集合与映射
信息架构设计里也藏着集合思维。一个内容平台上,文章有分类(科技、生活、财经),有标签(人工智能、美食探店、股市),分类更像是“全集的一个划分”——每篇文章只能属于一个分类,所有分类覆盖全部内容;标签则更像“多个交叠的集合”——一篇文章可以带多个标签,标签之间可以交叉。
分类是“划分”,标签是“标记”。前者是互斥的集合,后者是交叠的集合。这两种不同的集合结构,决定了我们在设计数据结构时的取舍:分类适合用在必须唯一的栏目场景;标签适合用在灵活的内容组织场景。你要是把分类硬做成标签,或者反过来,都会遇到结构性的别扭。
映射在信息架构里最常见的形态是“别名”和“路由”。一套资源要不要支持多个 URL 别名?一个错误码要不要映射到多语言的提示文案?这些本质上都是一张映射表。做得好的系统,这类映射一定集中管理,而不是散落在业务代码里,否则后期改一处文案就要全局搜索替换。
6. 实际踩坑记录:从哈希到集合并查询的教训
6.1 可变对象做键引发的幽灵 Bug
我印象最深的一次踩坑,是在一个配置系统里把列表当成字典的“状态键”。当时的业务场景是:把一组筛选条件(多个标签的组合)映射到某个运营策略上。我用 tuple 来规避列表不可哈希的问题,但列表里存的是自定义对象,而那个对象的 __eq__ 没有重写,导致两个字段完全相同的对象在哈希比较时始终不相等,结果同一个筛选条件反复插入多条策略,缓存命中率低得离谱。
这个问题的根子在于:哈希和相等必须一起设计。你只要自定义了一个类的相等逻辑,就必须同步考虑哈希逻辑,二者必须保持一致。现在我在团队里定的规矩是:凡是放进集合或作为键的自定义类型,必须重写 __eq__ 和 __hash__,并且参与哈希计算的字段必须是不可变字段。这个规矩来自实打实的线上故障。
6.2 集合遍历顺序导致的接口不稳定
另一个坑发生在对外 API 上。当时我们有一个接口返回一批“热门标签”,内部实现是先对标签做了一次 set 去重,再转成列表返回。上线之后收到反馈,同一个用户相邻两次请求返回的标签顺序不一样,导致前端页面每次刷新标签排列都会跳动。
排查后发现,就是因为 set 的遍历顺序在不同进程、不同插入顺序下都可能不同。解决办法很简单:去重之后按业务权重排序,或者用 dict.fromkeys() 保持首次出现的顺序。
这里要强调:集合做去重是对的,但去重之后如果要输出给外部,必须显式排序或者用保序结构。别指望“这次输出顺序正常,以后也会正常”,顺序这个事在集合里根本没有契约保障。
6.3 滥用集合运算导致的全表扫描
还有一次是在 SQL 里误用了 NOT IN 来排除一组子数据。那个查询在测试环境跑得还可以,一上生产数据量上去直接就慢查询告警。当时写的是:
sql复制SELECT * FROM products
WHERE category NOT IN (
SELECT category FROM banned_categories
);
这个写法在逻辑上没问题,但某些数据库版本对 NOT IN 的优化不理想。如果子查询返回的 banned_categories 集合过大,或字段有 NULL,执行计划可能变成全表扫描。换成 LEFT JOIN ... WHERE bc.category IS NULL 之后,性能明显改善。
这个案例说明的倒不是 NOT IN 一定不能用,而是:把集合逻辑翻译成 SQL 时,最终执行路径是由优化器决定的。理解集合语义能帮你写出逻辑正确的查询,但要写出高性能查询,还得理解优化器对每种集合运算的实现方式。
6.4 映射滥用后的可读性灾难
最后再说一个偏“软”的坑:滥用映射导致代码变成天书。
有些开发很痴迷用 dict 做“万能映射”,把原本能写得清清楚楚的分支逻辑全塞进字典里,比如用一个“策略字典”把不同的操作码映射到不同函数。逻辑简单时还好,一旦操作码变多、每个策略函数之间还有交互,这种“映射式设计”会让人看得头皮发麻。表面看是在用映射抽象,实际上是把业务语义塞进了一个又一个查表调用里,可读性和可调试性都急剧下降。
我现在的判断标准是:映射适合表达“静态、稳定、一一对应”的关系,比如状态码到提示文案、权限标识到权限描述;如果映射背后的值是动态计算的,或者不同分支之间存在复杂的副作用关系,还是老老实实写清晰的判断结构更好。集合与映射是强大的工具,但工具用得顺手的前提是你知道它的适用边界在哪里。
写到这里,其实已经把我这些年和集合与映射打交道的主要体会都交代完了。回看下来,这组概念并不高深,甚至可以说是基础中的基础,但恰恰是基础概念决定了你在复杂系统里能把问题看透到什么程度。我自己踩过的那些坑,没有一个是“数学没学好”导致的,但每一个都是“没有把数学思维映射到工程实现上”导致的。如果你能从这里带走一句话,我希望是这句:遇到任何数据关系问题,先问自己“这是集合运算还是映射查找”,再决定怎么写代码、建表还是设计接口。这个习惯会帮你少写很多循环,也少踩很多坑。
