☰
离散数学五大基本结构:集合、函数、序列、和式与矩阵的编程实践
2026/9/30 12:17:11 网站建设 项目流程

学离散数学最怕什么?不是符号多,也不是公式抽象,而是刚翻开教材就被“集合、函数、序列、和式和矩阵”这五个基本结构给整懵了。名字都眼熟,集合高中就学过,函数更是老朋友,但大学课程里的这套说法,和以前学的内容总感觉对不上号。再加上教材一上来就铺开一大堆记号、定义、性质,如果没人帮你把这五块内容串成一条线,很容易学着学着就变成“背定义,抄笔记,考试全靠考前突击”。

这篇东西我不想按教材的章节顺序走,我打算换一个更接地气的角度:把集合、函数、序列、和式、矩阵当成一套“描述计算问题的语言”来讲。你看完以后,再回去翻Rosen那本大厚书、或者屈婉玲老师的教材,会明显感觉那些定义不是孤立的,而是有关联的。这篇文章适合正在学离散数学的学生,也适合想补数学基础的程序员,我会从概念讲到代码,尽量让你今天看完,明天就能用起来。

1. 整体设计与思路拆解:这五个结构到底在讲什么

1.1 用“学生信息管理系统”把五个结构串起来

我讲课和写笔记时最喜欢用一个例子,就是学生信息管理系统。这个例子特别朴素,但能把五个结构串成一条完整的业务线:

  • 集合:全校学生的名单是一个集合,选了Python课的同学是一个集合,选了离散数学的同学是另一个集合。那么“两门课都选的人”是什么?“只选了一门的人”是什么?这就是集合的交集、并集、差集运算。
  • 函数:每个学生有一个学号,学号到姓名是一个映射,一个学号对应一个唯一姓名,这就是函数。函数描述的是“输入到输出”的对应规则。
  • 序列:把某个班级的同学按成绩从高到低排出来,得到一个有序的名单,这就是序列。序列的核心是“位置”和“顺序”,同样一批人,顺序不同就是不同的序列。
  • 和式:想求全班平均分,就得把所有人的成绩加起来,这里就是和式在起作用。和式解决的是“一堆数加起来怎么规范和高效地表示”的问题。
  • 矩阵:把每个学生每门课的成绩排成一张二维表,行是学生,列是课程,这张表就是矩阵。矩阵特别适合批量处理多维数据。

你会发现,这五个结构不是五个互不相干的数学玩具,它们分别对应着“有哪些对象、对象之间怎么对应、对象怎么排序、总量怎么计算、多维数据怎么组织”。学离散数学的时候,只要脑子里始终带着这个例子,概念就不容易散架。

1.2 为什么计算机领域离不开这五种结构

很多人问:我学编程,会写C++和Java就够了,为什么还要学集合、函数、矩阵这些东西?其实你已经在用了,只是没往数学上想。

集合对应数据库里的表操作,你写SQL时用UNION、INTERSECT,本质就是集合的并和交;你在Python里用set去重,就是在构建一个集合。函数就更不用说,编程里的函数、方法、接口,核心思想就是“接受输入,给出唯一输出”,离散数学里的函数对“唯一性”的要求,其实就是设计API时的接口规范。序列对应数组、链表、栈、队列,算法里的“最长上升子序列”“最大子序列和”问题,都是基于序列的计算。和式是算法复杂度分析的日常工具,你算一个双重循环的时间复杂度,就是在计算一个和式。矩阵就更直接了,图形图像里的缩放、旋转、平移,机器学习里的权重矩阵,都是矩阵运算的舞台。

我用一张表把数学概念和编程概念对应起来,这样你再看教材时会更有感觉:

数学概念编程概念典型应用
集合set、数据库表、去重操作查重、SQL的UNION/INTERSECT
函数函数、接口、映射API设计、lambda表达式
序列数组、链表、列表、字符串排序、搜索、动态规划
和式循环累加、reduce操作复杂度计算、统计求和
矩阵二维数组、NumPy数组图像变换、神经网络、图算法

1.3 学习路径与课程主线建议

我观察过很多同学,离散数学学得不好的,九成是学习方法出了问题。他们喜欢从第一页开始逐字逐句读,碰到一个定义就停下来背,结果背了十页就累了,前面的又忘了。

我的建议是:不要按教材顺序硬啃,而是先建立“主线”,再补“分支”。主线就是这五个基本结构之间的逻辑关系:先有集合,因为集合是最底层的“对象容器”;然后在集合之上定义函数,因为函数是两个集合之间的对应关系;接着把函数作用在“有序排列”上,得到序列;把多个数加起来,得到和式;把多个维度组合起来,得到矩阵。这条主线捋顺了,所有零碎定义都能挂上去。

教材方面,我比较推荐两本。一本是Rosen的《离散数学及其应用》,例子多、应用性强,英文原版读起来有困难的话可以配合中译本;另一本是屈婉玲老师的《离散数学》,国内教材里写得很清晰,习题质量高。但我想强调,不要只囤书不看,更不要只看PDF不手写。离散数学是一个“动手学科”,必须边看边做例题,概念才能变成你自己的。

2. 核心细节解析与实操要点

2.1 集合:从“属于”到“运算”的完整版图

集合概念本身不复杂,就是一个“把研究对象装在一起”的容器。关键要分清两个记号:元素和集合的关系用“属于”$a \in A$,集合和集合的关系用“包含于”${a} \subseteq A$。考试里最常见的低级错误,就是把“属于”和“包含于”搞混。判断技巧很简单:左边是单个元素就看属于,左边是带花括号的集合就看包含。

集合有三种常见表示方法:列举法、描述法、文氏图。列举法适合有限集合,比如$A={1,2,3}$;描述法适合元素规律明显的集合,比如$A={x \mid x是偶数, x>0}$;文氏图适合理解并、交、差、补这些运算关系。

集合的核心运算不多,我用一张表把所有运算的记法和Python对应操作列出来,方便你对照记忆:

运算数学记号含义Python写法
并集$A \cup B$属于A或属于B的所有元素A | B
交集$A \cap B$同时属于A和B的元素A & B
差集$A - B$属于A但不属于B的元素A - B
补集$\overline{A}$全集中不属于A的元素无直接运算符,需自己定义
对称差$A \oplus B$属于A和B中恰好一个集合的元素A ^ B

此外,集合有几个“看起来简单但容易被忽略”的性质:空集是任何集合的子集,任何集合都是它自身的子集。如果一个集合有$n$个元素,那么它的幂集$P(A)$有$2^n$个元素。这个结论在做“3个元素的集合有多少拓扑”这类题时会用到,本质就是“每个元素选或是不选”,共有$2^n$种组合。

集合运算还满足交换律、结合律、分配律和德摩根律。德摩根律是:$\overline{A \cup B} = \overline{A} \cap \overline{B}$,$\overline{A \cap B} = \overline{A} \cup \overline{B}$。理解它有个生活化类比:你说“我没有苹果也没有香蕉”,等于“我没有苹果,而且也没有香蕉”;你说“我不是又高又富有”,等价于“我不高或者我不富有”。这个类比比死记公式靠谱得多。

2.2 函数:一对一的“接口约定”

函数在离散数学里的定义是:从集合A到集合B的一个映射,使得A中的每个元素都对应B中唯一的一个元素。写为$f: A \to B$。你注意“唯一”这个词,这正是函数和一般关系的区别。关系允许一个输入对应多个输出,函数不允许。

判断函数性质时有三个重要概念,我建议用“查户口”的方式理解:

  • 单射:对应关系“不会撞车”。不同的$x$一定对应不同的$f(x)$。用程序员的说法,就是主键不可以重复。
  • 满射:B中的每个元素“都有对象”。即每个可能的输出都至少被某个输入映到一次。用程序员的说法,就是值域等于陪域。
  • 双射:既单射又满射,一对一且全覆盖。双射的存在意味着两个集合的大小“一样多”。

遇到具体函数时,分类判定可以用这张表:

函数性质判定方法示例
单射如果$f(x_1)=f(x_2)$能推出$x_1=x_2$$f(x)=2x$是单射,$f(x)=x^2$不是
满射值域恰好等于陪域,B中没有落空的元素从${1,2,3}$到${a,b}$的满射例子很多
双射既单射又满射$f(x)=x+1$,从整数集到整数集是双射

函数复合也是一个高频考点。$f \circ g$表示先做$g$,再做$f$,即$(f \circ g)(x)=f(g(x))$。这里特别容易搞反顺序,我每次做题都会先在心里读一遍:“这个符号右边的先执行”。如果$f$是双射,那么它有反函数$f^{-1}$,反函数的意思就是把箭头全反过来,还是一一对应。

我在实际编码里还有个体会:离散数学里的函数很像编程里的纯函数——同样的输入永远给出同样的输出,没有副作用。理解了这一点,函数式编程里的“不可变性”和“引用透明”就不难理解了。

2.3 序列与和式:排序世界里的“循环表达式”

序列就是按照一定顺序排列的元素,写为$a_1, a_2, a_3, \ldots, a_n$。序列与集合最大的区别是:集合里的元素没有顺序、不能重复,序列里的元素有序、可以重复。简单说,${1,2,3}$和${3,2,1}$是同一个集合,但$(1,2,3)$和$(3,2,1)$是两个不同的序列。

序列有两种表示方式:显式公式和递推公式。显式公式直接给出$a_n$关于$n$的表达式,比如$a_n=3n+1$;递推公式给出前项和后项的关系,比如$a_{n+1}=a_n+2$,需要给定初始值。大名鼎鼎的斐波那契数列就是递推公式的典型:$F_1=1,F_2=1,F_n=F_{n-1}+F_{n-2}$。递推在算法里就是“状态转移方程”,动态规划的本质,就是在序列上做递推。

等差序列和等比序列的前$n$项和公式,是考试和复杂度计算的常客。等差数列求和是$\frac{n(a_1+a_n)}{2}$,等比数列求和是$\frac{a_1(1-q^n)}{1-q}$(当$q \neq 1$时)。我建议你把这两个公式当成“肌肉记忆”,尤其是等差数列公式,以后分析算法复杂度时经常会碰到。

和式的表示也很简单:$\sum_{i=1}^{n} a_i$,意思是把$a_i$从$i=1$一直加到$i=n$。它最容易被忽视的地方,是求和上下标的处理。做题时一定要先看清是从几加到几,比如$\sum_{i=0}^{n} 2^i$和$\sum_{i=1}^{n} 2^i$,结果差一个$1$。常用求和公式我建议背这几个:$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$,$\sum_{i=1}^{n} i^2 = \frac{n(n+1)(2n+1)}{6}$,$\sum_{i=0}^{n} 2^i = 2^{n+1}-1$。

从编程视角看,序列就是一个数组或链表,和式就是一个循环累加。你每次写sum += a[i],本质上就是在计算一个和式;你每次用for i in range(1, n+1),就已经在枚举序列了。

2.4 矩阵:二维数据的“批量处理说明书”

矩阵可以看成是一个矩形的数表,几行几列写成$m \times n$。比如$3 \times 3$矩阵就是三行三列。矩阵里的数称为元素,用双下标表示,比如$a_{ij}$表示第$i$行第$j$列的元素。编程里数组下标是从0开始,而数学里矩阵下标从1开始,做题和写代码时要特别留心这个差异。

矩阵的基本运算有三种:加法、数量乘法、矩阵乘法。矩阵加法要求两个矩阵维度完全相同,就是对应位置相加。矩阵乘法稍微特殊,$A$是$m \times n$,$B$是$n \times p$,乘积$C=A \times B$是$m \times p$,其中$c_{ij}=\sum_{k=1}^{n} a_{ik}b_{kj}$。可以理解为:C的第$i$行第$j$列元素,等于A的第$i$行和B的第$j$列对应元素相乘再求和。

矩阵乘法有个非常经典的坑:不满足交换律。也就是$A \times B$一般不等于$B \times A$。很多人初学的时候习惯性按普通乘法套用,做题一下子就错了。记住一句话:矩阵乘法是“行列点积”,行乘过去,列加过来,方向不能反过来。

除了基本运算,特殊矩阵也经常考。零矩阵是所有元素都是0;单位矩阵$I$是主对角线为1、其余为0的方阵,它相当于矩阵世界里的“1”,任何矩阵乘单位矩阵都等于它自己;对角矩阵是主对角线以外全是0的矩阵。对称矩阵满足$A^T=A$,即转置后等于自身。

矩阵的行列式、逆矩阵、特征值分解,是进阶内容。特征值分解的本质,是把一个矩阵分解成“特征向量矩阵 × 特征值对角矩阵 × 特征向量逆矩阵”的形式。你不需要在基本离散结构这一章就往深里钻,但可以先建立一个直觉:特征值告诉我一个矩阵在各个方向上的“伸缩比例”,这个思想在机器学习的主成分分析、图像压缩里都会用到。

3. 实操过程与核心环节实现:用代码验证数学结论

3.1 用Python实现集合运算与可视化

纸上谈兵不够,我强烈建议你打开Python环境,亲手把集合运算跑一遍。Python里的set类型天然支持数学集合运算,代码非常直观:

A = {1, 2, 3, 4} B = {3, 4, 5, 6} print(A | B) # 并集:{1, 2, 3, 4, 5, 6} print(A & B) # 交集:{3, 4} print(A - B) # 差集:{1, 2} print(B - A) # 差集:{5, 6} print(A ^ B) # 对称差:{1, 2, 5, 6}

再实现一个求幂集的函数,用来验证“$n$个元素的集合,幂集有$2^n$个元素”这个结论:

from itertools import combinations def power_set(s): s = list(s) result = [] for r in range(len(s) + 1): for comb in combinations(s, r): result.append(set(comb)) return result A = {1, 2, 3} ps = power_set(A) print(len(ps)) # 输出 8 print(ps)

如果你想把文氏图画出来,可以装matplotlib-venn这个库。画图的意义不是炫技,而是让你直观看到“交集是两个圈重叠的部分”“差集是一个圈去掉重叠部分剩下的区域”。我看过不少同学画了几张文氏图之后,德摩根律就再也没记错过,因为脑子里的图像已经形成了。

实际操作中有一个坑必须提醒:Python的set元素必须是不可变类型,也就是说你不能把列表或字典放进集合里。想放一个“包含多个元素的组合”,先转成元组tuple。这个细节点在刷题时经常坑人。

3.2 用Python判断函数性质并演示函数复合

函数在集合论里的本质是特殊的二元关系。我可以用字典来模拟一个有限函数,比如$f:{1,2,3}\to{a,b,c}$,定义为{1: 'a', 2: 'b', 3: 'c'}。判断单射和满射的代码非常简单:

def is_injective(f): # 单射:不同的输入,输出各不相同 return len(set(f.values())) == len(f.values()) def is_surjective(f, codomain): # 满射:输出集合覆盖整个陪域 return set(f.values()) == set(codomain) f = {1: 'a', 2: 'b', 3: 'c'} print(is_injective(f)) # True print(is_surjective(f, ['a', 'b', 'c'])) # True

函数复合的实现也很直观。先做内层函数g,再做外层函数f:

def compose(f, g): # 返回 f ∘ g,表示先 g 后 f result = {} for x, gx in g.items(): result[x] = f[gx] return result f = {'a': 1, 'b': 2, 'c': 3} g = {1: 'a', 2: 'b', 3: 'c'} h = compose(f, g) print(h) # {1: 1, 2: 2, 3: 3},这就是恒等映射

我建议你把代码跑一遍,然后对比教材里的复合函数定义。你就会发现,“$f \circ g$是先执行$g$”这件事,在代码里是“先查g的表,再拿结果去查f的表”,非常直白。

3.3 用Python生成序列并计算和式

序列和和式的代码实现,是理解“循环”和“数学归纳”的绝佳抓手。比如生成一个等差数列,并验证前$n$项和公式:

a1 = 1 d = 3 n = 10 seq = [a1 + (i - 1) * d for i in range(1, n + 1)] # 暴力求和 total = sum(seq) # 公式求和 formula = n * (a1 + seq[-1]) // 2 print(seq) # [1, 4, 7, 10, 13, 16, 19, 22, 25, 28] print(total) # 145 print(formula) # 145

斐波那契数列用递推来实现,也是面试里反复出现的题:

def fibonacci(n): a, b = 1, 1 for _ in range(n - 1): a, b = b, a + b return a print([fibonacci(i) for i in range(1, 10)]) # [1, 1, 2, 3, 5, 8, 13, 21, 34]

再介绍一个实际项目里很有用的技巧:如果在线性序列上做累加,改用生成器表达式而不是一次性生成列表,可以省不少内存:

total = sum(i * i for i in range(1, 1000001)) print(total)

这个写法本质上就是和式$\sum_{i=1}^{n} i^2$的代码表达。注意,这里的i * i for i in range(...)是一个生成器,不会一次性把100万个平方数全部存进内存,性能比列表推导式好很多。

3.4 用NumPy完成矩阵运算与特征值分解

矩阵运算在Python里最常用的工具是NumPy。如果你做图像处理、数据分析或者机器学习,几乎天天和它打交道。基本操作如下:

import numpy as np A = np.array([[1, 2], [3, 4]]) B = np.array([[5, 6], [7, 8]]) print(A + B) # 矩阵加法 print(2 * A) # 数量乘法 print(A @ B) # 矩阵乘法,注意用的是 @ print(A.T) # 转置

初学者最容易踩的坑是混淆*和@。A * B在NumPy里是对应元素相乘(Hadamard积),不是数学上的矩阵乘法;A @ B才是线性代数里的矩阵乘法。如果写成A * B,系统不报错,但结果完全不对,这种隐蔽错误最耗调试时间。

特征值分解的代码只要一行:

A = np.array([[2, 1], [1, 2]]) eigenvalues, eigenvectors = np.linalg.eig(A) print("特征值:", eigenvalues) print("特征向量:\n", eigenvectors)

输出结果里,特征值表示矩阵作用在对应特征向量方向上的伸缩比例。这个概念在基本离散结构阶段不需要深究,但提前体验一下,后面学到矩阵论或机器学习时会有亲切感。

NumPy矩阵下标也需要注意,它遵循Python从0开始,不是数学里的从1开始,所以A[0][1]对应矩阵的第1行第2列,也就是数学记号里的$a_{12}$。写代码和读教材时,下标要来回切换,这个转换能力本身就是一种实战能力。

4. 常见问题与排查技巧实录

4.1 概念易混点速查:考前最好过一遍

我把这些年批改作业、答疑时最常看到的错误汇总成了一张表。这张表我建议你在期末复习时过一遍,基本能覆盖80%的“概念混淆”失分点:

易混点正解错误理解
属于 vs 包含$a \in A$是元素属于集合,${a} \subseteq A$是集合包含于集合把两者混用
函数 vs 关系每个输入有唯一输出觉得多值也能叫函数
单射 vs 满射单射看输出是否重复,满射看输出是否全覆盖把单射当成满射
矩阵乘法顺序$AB$中A的列数必须等于B的行数,且一般$AB \neq BA$按普通乘法交换顺序
求和上下标$\sum_{i=0}^{n}$和$\sum_{i=1}^{n}$结果差第一项直接忽略下标范围
集合元素是否重复集合无重复元素把序列的有序且可重复套到集合上

还有一个我之前反复讲过的点:集合里的“元素”可以是集合本身,比如${{1,2},3}$,这种情况看清外层花括号就行。做题时不要想当然地“把括号去掉”,每一步都要回到定义。

4.2 编程实现时的常见报错与心理预期

如果你跟着上面的代码实操,大概率会遇到几个小问题,我提前说一下排查方法。

第一个是TypeError: unhashable type: 'list'。这个报错出现在你试图把列表放入集合,或者把列表作为字典的键时。解决方案是先把列表转成元组,比如set([1, 2])这种写法没问题,但set([[1,2]])就会报错,要写成set([(1,2)])。

第二个是NumPy中矩阵乘法结果不符合预期。检查一下你有没有把@写成*。如果两个矩阵形状是(2,2),A * B和A @ B都能运行,但意义完全不同。这提醒我们,代码能跑不代表逻辑正确,一定要打印中间结果验证。

第三个是复合函数时KeyError。比如前面compose的例子,如果f的键域和g的值域不一致,就会找不到对应的键。这其实对应数学里的一个前提条件:$g$的值域必须是$f$的定义域的子集。数学条件在代码里就表现为“查表不能查空”,两者是一致的。遇到这类报错,你要检查的不是代码语法,而是两个映射的定义域和值域是否匹配。

还有一个小坑是浅拷贝问题。用list.copy()复制二维列表时,内层列表仍然是同一个对象,修改一个会影响另一个。矩阵操作用NumPy可以避免很多这种问题,但如果你坚持用纯Python写二维列表,赋值时请用深拷贝copy.deepcopy()。

4.3 自学资源与工具推荐(踩坑后的个人推荐)

最后聊点个人向的资源推荐。教材方面,我前面提的Rosen和屈婉玲两本都是经典,但如果你觉得Rosen的书太厚、太容易劝退,可以先用屈婉玲的教材打底,再回头啃Rosen里的应用例子。刷题时不要只做选择填空,一定要动手写解答题,尤其要写“证明两个集合相等”这类题,它是训练逻辑推导最好的方式。

在线工具方面,函数图像绘制可以用Desmos或GeoGebra,输入公式立刻出图像,对理解函数性质帮助很大。矩阵计算可以搜在线矩阵计算器,验证自己手算的乘法结果,省去检查算错的烦恼。但工具再好,也代替不了手推。我见过的学生里,凡是课上划水、只靠工具对答案的,期末考试基本都吃亏。

笔记方法上,我建议做“概念-例子-代码”三段式笔记。每个概念右边配一个数学例子,下面再配一段Python代码。这样复习的时候,数学概念和程序实现互相印证,比单看教材效率高很多。在互联网上你也能找到不少别人整理好的“离散数学笔记”,但我的体会是:别人的笔记只能用来查缺补漏,自己动手写一遍才是真正过脑子。

最后再分享一个小技巧。学离散数学,尤其是基本离散结构这一章,真正管用的办法是“双向翻译”:看到一个数学公式,想一想它对应的Python代码长什么样;看到一段循环代码,想一想它对应的数学公式是什么。比如sum(a[i] for i in range(1, n+1))和$\sum_{i=1}^{n}a_i$,练上几道题,你就再也不会觉得数学符号和代码是两套语言了。这套“翻译思维”不只是为了应付期末考,它会在你以后读论文、看算法资料、设计系统的时候,持续给你回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询