▤书架 ← 返回索引
▶目录

阶段 07 讲义

数学 11h:高数 · 凹凸性与函数作图
编程 9h:列表 · 可变性陷阱


第一部分 · 数学

1 凹凸性:曲线是"往上弯"还是"往下弯"

单调性(阶段 6)说的是曲线往上还是往下走。
凹凸性说的是曲线怎么弯,是"越来越陡"还是"越来越平"。

1.1 ⚠️ 先解决一个命名混乱(很重要)

"凹"和"凸"这两个字,不同教材的用法是相反的。这不是你记错,两套用法确实不统一。

所以别记字,记形状。

形状 $f''$ 国内主流叫法(同济) 英文 直观
∪ 开口向上 $f''>0$ 凹 concave up 像碗,中间凹下去
∩ 开口向下 $f''<0$ 凸 concave down 像拱门,中间鼓起来
     ∪ 形(f''>0)              ∩ 形(f''<0)

        ╲    ╱                     ╱‾‾‾‾╲
         ╲  ╱                    ╱        ╲
          ╲╱                    ╱            ╲
      开口向上,像碗            开口向下,像拱门
      "凹"(同济)              "凸"(同济)
      concave up               concave down

⚠️ 考试/论文里遇到"凹凸"两个字,先确认对方用的是哪套。
唯一可靠的判断是开口方向($f''$ 的正负)。
本讲义统一用同济约定:$f''>0$ 叫凹(∪),$f''<0$ 叫凸(∩)。

1.2 判据

  • $f''(x) > 0$ 在区间上恒成立 → 曲线是凹的(∪,开口向上)
  • $f''(x) < 0$ 在区间上恒成立 → 曲线是凸的(∩,开口向下)

直觉:$f''$ 是 $f'$ 的导数,即斜率的变化率。

1.3 完整例子

判断 $f(x)=x^3-3x$ 的凹凸区间。

第一步,求二阶导:

$$f'(x)=3x^2-3, \qquad f''(x)=6x$$

第二步,找 $f''=0$ 的点,分段判断符号:

$$f''=6x=0 \implies x=0$$

区间 测试点 $f''$ 凹凸
$(-\infty,0)$ $x=-1$ $-6<0$ 凸(∩)
$(0,+\infty)$ $x=1$ $6>0$ 凹(∪)

答:在 $(-\infty,0)$ 上凸,在 $(0,+\infty)$ 上凹。

2 拐点

拐点:凹凸性发生改变的点。

在图上,就是曲线从"往上弯"变成"往下弯"(或反过来)的那个转折点。

拐点的判定:
若 $f''(x_0)=0$(或不存在),且 $f''$ 在 $x_0$ 两侧异号,
则 $(x_0, f(x_0))$ 是拐点。

⚠️ "两侧异号"这个条件不能少,这是最容易错的地方。

2.1 关键反例:$f''=0$ 但不是拐点

看两个函数,都在 $x=0$ 处有 $f''(0)=0$:

函数 $f''$ $x=0$ 两侧 是拐点吗
$f(x)=x^3$ $6x$ 左负右正,异号 ✅ 是拐点
$f(x)=x^4$ $12x^2$ 两侧都是正,同号 ❌ 不是拐点

数值验证 $f=x^4$:

$x$ $-1$ $-0.5$ $0$ $0.5$ $1$
$f''=12x^2$ $12$ $3$ $\mathbf{0}$ $3$ $12$

两侧全是正的(都是 ∪ 形),在 $x=0$ 处只是"平了一下",凹凸性没变。

💡 所以 $f''=0$ 只是"嫌疑点",和 $f'=0$ 只是极值的嫌疑点一样。
必须检查两侧符号是否改变。

2.2 完整例子

求 $f(x)=x^3-3x$ 的拐点。

由上节:$f''=6x$,在 $x=0$ 处 $f''=0$,且两侧异号(左负右正)。

所以 $(0, f(0)) = (0, 0)$ 是拐点。

3 渐近线 ⭐

渐近线:曲线无限接近、但(通常)永不相交的直线。

画图时必须先找渐近线,它决定了图的"骨架"。

3.1 三种渐近线

① 垂直渐近线(竖着的那根)

发生在 $f$ 趋于无穷的断点处:

$$\lim_{x\to x_0} f(x) = \infty \implies x = x_0 \text{ 是垂直渐近线}$$

怎么找:看分母为 0 的点(以及其它无定义点)。

② 水平渐近线(横着的那根)

$$\lim_{x\to+\infty} f(x) = b \quad\text{或}\quad \lim_{x\to-\infty} f(x) = b \implies y = b$$

怎么找:算 $x\to\pm\infty$ 时的极限。注意两个方向可能不同。

③ 斜渐近线(斜着的那根)

若 $\lim_{x\to\infty}\frac{f(x)}{x} = k \ne 0$ 且 $\lim_{x\to\infty}[f(x)-kx] = b$,则 $y=kx+b$ 是斜渐近线。

怎么找:先除一次看斜率 $k$,再减掉看截距 $b$。
(有水平渐近线时就不会有斜渐近线,$k=0$ 的那种情况归水平。)

3.2 完整例子 A:三种都有的函数

求 $f(x)=\dfrac{x^2+1}{x}$ 的渐近线。

化简(关键一步):

$$f(x) = x + \frac1x$$

垂直渐近线:$x=0$ 处分母为 0,且 $\lim_{x\to0^+}f(x)=+\infty$、$\lim_{x\to0^-}f(x)=-\infty$。
→ $x=0$ 是垂直渐近线。

水平渐近线:$\lim_{x\to\infty}\left(x+\frac1x\right)=\infty$,不为有限值。
→ 没有水平渐近线。

斜渐近线:

$$k=\lim_{x\to\infty}\frac{f(x)}{x} = \lim_{x\to\infty}\left(1+\frac{1}{x^2}\right)=1$$
$$b=\lim_{x\to\infty}\left[f(x)-1\cdot x\right]=\lim_{x\to\infty}\frac1x=0$$

→ $y=x$ 是斜渐近线。

数值验证:$f(x)-x = \frac1x$,$x=100$ 时是 $0.01$,$x=10000$ 时是 $0.0001$ → 趋于 0 ✓

3.3 完整例子 B:$\dfrac{x^2}{x+1}$

多项式除法(把假分式变"整式 + 真分式"):

$$\frac{x^2}{x+1} = (x-1) + \frac{1}{x+1}$$

垂直渐近线:$x=-1$(分母为 0,极限为 $\infty$)。

斜渐近线:$y=x-1$。

验证:$f(x)-(x-1)=\frac{1}{x+1}$,$x\to\infty$ 时趋于 0 ✓

💡 多项式除法是求斜渐近线的快捷方式。
分子次数 = 分母次数 + 1 时,必有斜渐近线。会做除法就直接除。

4 函数作图:完整流程

把前面学的全串起来。拿到一个函数,按这个顺序做:

① 定义域,先确定能取哪些 $x$
② 找渐近线,垂直 / 水平 / 斜(决定图的骨架)
③ 一阶导,单调区间 + 极值点
④ 二阶导,凹凸区间 + 拐点
⑤ 描几个关键点,极值点、拐点、与坐标轴的交点
⑥ 连线,按单调性和凹凸性把曲线画出来

4.1 完整例子

作 $f(x)=x^3-3x$ 的图像。

步骤 结果
①定义域 $\mathbb{R}$(多项式,处处有定义)
②渐近线 无(多项式没有渐近线)
③一阶导 $f'=3x^2-3$,驻点 $x=\pm1$
增:$(-\infty,-1)\cup(1,+\infty)$;减:$(-1,1)$
极大 $f(-1)=2$,极小 $f(1)=-2$
④二阶导 $f''=6x$,拐点 $(0,0)$
凸(∩):$(-\infty,0)$;凹(∪):$(0,+\infty)$
⑤关键点 $(-1,2)$ 极大、$(0,0)$ 拐点、$(1,-2)$ 极小
与 $x$ 轴交点:$x(x^2-3)=0 \Rightarrow x=0,\pm\sqrt3$

连线:从 $(-\infty)$ 出发,$x<-1$ 段递增且凸(∩,越来越平)→ 到极大 $(-1,2)$
→ 递减,穿过拐点 $(0,0)$,凹凸从凸变凹 → 到极小 $(1,-2)$ → 之后递增且凹(∪)

💡 一阶导管"走的方向",二阶导管"弯的方式"。两个信息合起来,图的形状就唯一了。


5 🧠 这跟认知科学有什么关系

概念性内容,无法用代码验证,请对照文献。

拐点 ↔ 学习/发展曲线上的「转折点」。

阶段 3 讲过:学习曲线是"先陡后平"($f''<0$)。但真实的曲线往往不是单一形状,
它可能在某个阶段从"先陡后平"变成"先平后陡",那一点就是拐点。

这在发展心理学里很常见:

现象 数学描述
词汇爆发期(vocabulary spurt) 增长曲线从平缓变陡,出现拐点
高原期 / 瓶颈期 进步速度降到接近 0
"开窍了" 从慢到快的转折,出现拐点

为什么这个视角有用:当你看到"某能力在 X 岁出现快速增长",你问的下一个问题应该是
"这是一个陡峭的线性增长,还是一个有拐点的 S 型曲线?",两者的理论含义完全不同。
"有拐点"意味着背后可能有质变(新机制的加入),而不只是量的积累。

数据上怎么找拐点:拐点是二阶导变号的地方,在离散数据里就是差分的变化率变号。
不过要小心,噪声会让差分剧烈波动,直接从噪声数据估二阶导很不稳。
实际做法需要平滑,这是后面数值分析/统计要学的。

⚠️ 重要提醒:上面"词汇爆发期是拐点"是一种建模视角。
发展心理学里对"词汇爆发"是否真实存在本身就有争论(可能是个体差异和平滑增长的叠加)。
数学($f''$ 变号)我能保证;心理学结论请以文献为准。


6 数学作业 S7

第 5 题提示(多项式除法):

import sympy as sp
x = sp.Symbol('x')
q, r = sp.div(x**2, x-1)      # 商和余数
print(f"{x**2} / ({x-1}) = ({q}) + ({r})/({x-1})")

第 8 题参考代码:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-10, 10, 500)
x = x[np.abs(x) > 0.15]          # 避开 x=0 附近(那里趋于无穷)
f = (x**2 + 1) / x

plt.plot(x, f, label="$f(x)=\\frac{x^2+1}{x}$")
plt.plot(x, x, "--", color="gray", label="斜渐近线 $y=x$")
plt.axvline(0, linestyle=":", color="black", label="垂直渐近线 $x=0$")
plt.ylim(-10, 10)
plt.xlabel("x"); plt.ylabel("y"); plt.legend()
plt.title("渐近线")
plt.show()

第二部分 · 编程

7 列表回顾与切片

列表是有序、可变的序列。阶段 1 见过,现在系统过一遍切片(这是列表最常用的操作之一)。

lst = [10, 20, 30, 40, 50]
写法 结果 含义
lst[1:4] [20, 30, 40] 下标 1 到 3(不含 4)
lst[:3] [10, 20, 30] 从头到下标 2
lst[2:] [30, 40, 50] 下标 2 到尾
lst[-1] 50 最后一个
lst[-2:] [40, 50] 最后两个
lst[::2] [10, 30, 50] 步长 2
lst[::-1] [50, 40, 30, 20, 10] 反转

⚠️ 切片不含终点,和 range 一样。这是设计上的一致(都遵循"左闭右开")。

💡 lst[::-1] 是最常用的反转写法。比写循环优雅得多。

切片返回新列表(不是原地修改):

lst = [10, 20, 30, 40, 50]
sub = lst[1:4]        # 新列表
sub[0] = 999
print(lst)            # [10, 20, 30, 40, 50]  ← 原列表没变

8 常用的列表方法

方法 作用 返回
lst.append(x) 末尾加一个元素 None(原地改)
lst.extend(other) 把另一个列表的元素逐个加入 None(原地改)
lst.insert(i, x) 在下标 i 处插入 None(原地改)
lst.remove(x) 删除第一个值为 x 的元素 None(原地改)
lst.pop(i) 删除并返回下标 i 的元素 被删的元素
lst.sort() 原地排序 None
lst.index(x) 返回 x 首次出现的下标 下标
lst.count(x) x 出现几次 次数

💡 append vs extend(最容易混的一对):

a = [1, 2]
a.append([3, 4])     # → [1, 2, [3, 4]]   把整个列表当一个元素
b = [1, 2]
b.extend([3, 4])     # → [1, 2, 3, 4]     把元素逐个加进去

⚠️ sort vs sorted:
- lst.sort() 原地排序,返回 None
- sorted(lst) 返回新列表,原列表不变

踩坑现场:

a = [3, 1, 2]
b = a.sort()          # ❌ b 是 None!

因为 sort() 返回 None。想拿排序结果用 sorted(a),或先 a.sort() 再用 a。

⚠️ append、extend、sort 都返回 None,这是"原地修改"方法的一致约定。
看到返回值是 None,就说明它是原地改的。

9 ⚠️⚠️ 可变性陷阱(本阶段重点)

这也是阶段 1 埋下的伏笔。

9.1 别名:b = a 不是拷贝

a = [1, 2, 3]
b = a              # ⚠️ 这不是"复制",是"再贴一个标签"
b.append(4)
print(a)           # [1, 2, 3, 4]   ← a 也变了!
print(b)           # [1, 2, 3, 4]

为什么:回忆阶段 1,变量是标签。b = a 让 b 和 a 贴在同一个列表对象上。
append 是原地修改,所以两个标签看到的都变了。

验证一下是同一个对象:

a = [1, 2, 3]
b = a
print(id(a) == id(b))    # True  ← 同一个对象

💡 id() 返回对象的内存地址。两个变量 id 相同 = 同一个对象。

9.2 == 和 is 的区别

比较 问的是 例子
== 值一样吗 [1,2] == [1,2] → True
is 是同一个对象吗 [1,2] is [1,2] → False
p = [1, 2, 3]
q = [1, 2, 3]
r = p
print(p == q)    # True    值相同
print(p is q)    # False   两个不同的列表
print(p is r)    # True    同一个对象

⚠️ 判断"是不是同一个"要用 is,不要用 ==。
特例:判断 None 一定用 is None(None 是单例)。

9.3 怎么正确地复制

三种浅拷贝(效果一样,任选):

a = [1, 2, 3]

b = a.copy()       # 方法一
c = list(a)        # 方法二
d = a[:]           # 方法三:全切片

b.append(99)
print(a)           # [1, 2, 3]   ← 没被影响 ✓

⚠️ b = a 是别名,b = a.copy() 才是拷贝,一字之差。

9.4 ⚠️ 浅拷贝的陷阱:嵌套列表

浅拷贝只复制最外层。里面的列表还是共享的:

import copy

orig = [[1, 2], [3, 4]]
shallow = orig.copy()

shallow[0].append(99)
print(orig)          # [[1, 2, 99], [3, 4]]   ← 内层被改了!

想彻底独立,要用深拷贝:

import copy

orig = [[1, 2], [3, 4]]
deep = copy.deepcopy(orig)
deep[0].append(99)
print(orig)          # [[1, 2], [3, 4]]   ← 完全没被影响 ✓
方式 复制到几层 什么时候用
b = a 不复制(别名) 几乎不用(除非真的想共享)
a.copy() / a[:] / list(a) 只第一层 列表里都是不可变元素时
copy.deepcopy(a) 所有层 列表里嵌套了可变对象时

💡 怎么选:问自己"我的列表里还有列表/字典吗?"
- 没有 → .copy() 够了
- 有 → 而且你要改内层 → 用 deepcopy

deepcopy 更慢,所以别无脑用。但用错了会出静默 bug。

9.5 列表作为函数参数

结合阶段 6 讲的"参数传的是引用":

def add_bad(lst):
    lst.append("新元素")      # 原地改 → 调用者的列表被改了

def add_good(lst):
    new = lst.copy()          # 先拷贝
    new.append("新元素")
    return new                # 返回新的

my = [1, 2]
add_bad(my)
print(my)                     # [1, 2, '新元素']  ← 被改了!

my2 = [1, 2]
result = add_good(my2)
print(my2)                    # [1, 2]           ← 没变 ✓
print(result)                 # [1, 2, '新元素']

💡 经验:函数不应该偷偷改调用者传进来的列表,
除非从函数名就能看出来(比如 lst.sort() 这种明摆着原地改的)。
想改就先 copy(),或者返回新列表。


10 🔧 技能卡 7:画内存图,理解"可变"

可变性的坑这么难躲,因为你在脑子里"看不到"它。

这张卡给你一个工具:把变量画成"标签和盒子"。

10.1 画法:标签 → 对象

规则:
- 变量画成标签(箭头)
- 对象画成盒子(用 id 标识)
- = 是"把标签贴到盒子上",不是"复制盒子"

例子 1:a = [1,2,3] 然后 b = a

   a ─┐
      ├──→ ┌─────────────┐
   b ─┘    │ [1, 2, 3]   │  ← 同一个盒子!
            └─────────────┘

两个标签,一个盒子。所以改盒子,两边都看得见。

例子 2:b = a.copy()

   a ──→ ┌─────────────┐
         │ [1, 2, 3]   │
         └─────────────┘

   b ──→ ┌─────────────┐
         │ [1, 2, 3]   │  ← 新盒子(值一样,但不同对象)
         └─────────────┘

两个标签,两个盒子。互不影响。

例子 3:不可变的整数,v = 100

   v ──→ ┌─────┐
         │ 100 │
         └─────┘

v = 100 不是把 5 改成 100,是让 v 改贴到一个新盒子上。原来那个盒子动都没动。

   v ══╗
       ╚═→ ┌─────┐
           │ 100 │
           └─────┘
   (原来贴的 5 那个盒子,如果没人贴,就被回收了)

10.2 用这个模型解释所有坑

现象 内存图里发生了什么
b = a 后改 b 影响 a 两个标签贴同一个盒子
v = 100 影响不到外面 只是换标签,没动盒子
lst.append(x) 影响外面 改了盒子里面的东西
lst.copy() 后互不影响 造了新盒子
deepcopy 后连内层也独立 连盒子里的盒子也一起造了新的

10.3 画图练习法(推荐照做)

卡住的时候,拿张纸,把标签和盒子画出来。比如这段:

a = [1, 2]
b = a
c = a.copy()
b.append(3)
c.append(4)
print(a, b, c)

画图(先自己画,再看下面):

   a ─┐
      ├──→ ┌────────────┐   append(3) 改这里
   b ─┘    │ [1, 2, 3]  │
            └────────────┘

   c ──→   ┌────────────┐   append(4) 改这里
           │ [1, 2, 4]  │
           └────────────┘

答案:a = [1,2,3],b = [1,2,3],c = [1,2,4]。

💡 为什么这招管用:可变性的 bug 之所以难找,是因为代码里看起来像是"两个东西",
实际是"一个东西"。画图把这个隐形的共享变可见。
专业程序员 debug 这类问题时,脑子里就在画这个图。


11 🧠 这跟认知科学有什么关系(编程侧)

概念性内容,无法用代码验证,请对照文献。

别名陷阱 ↔ 实验数据的「意外污染」。

这是编程里最危险的一类 bug:它不报错,只是结果悄悄错了。
科研数据处理里,它也以同样的形式出现:

场景 编程里 数据处理里
别名 raw = data,处理 raw 结果改了 data 原始数据被"就地"清洗,无法回退
浅拷贝 嵌套结构内层仍共享 只复制了主表,被试信息表被连带修改
静默错误 不报错,结果错 分析跑完了,但用的是被改过的数据

科研里的标准对策,和编程里的对策一一对应:

问题 编程对策 数据对策
原数据被改 data.copy() 保留原始数据只读,分析在副本上做
忘记保留 — 原始数据永不覆盖,只增不改
无法追溯 版本控制 数据清洗记录每一步

💡 这就是"可复现研究"里"原始数据不可变"原则的由来。
不是洁癖,是一个和 raw = data 完全同源的、会真实毁掉分析结果的风险。

deepcopy ↔ 嵌套结构的完整复制。

被试数据常常是嵌套的:一个被试 → 多个试次 → 多个通道的时间序列。
浅拷贝只会复制最外层,改了某个试次,原数据也跟着变。
这提醒你:处理嵌套数据时,要检查"我复制到底了没有"。

⚠️ 上面是结构性类比(同一个失效模式),不是心理学理论。
实验数据处理的具体规范请以你所在领域的标准(如 BIDS)为准。


12 编程作业 S7

第 2 题参考写法:

def my_sort(lst):
    result = lst.copy()            # ← 关键:先拷贝,不动原表
    n = len(result)
    for i in range(n):
        for j in range(n - 1 - i):
            if result[j] > result[j + 1]:
                result[j], result[j + 1] = result[j + 1], result[j]
    return result

nums = [5, 3, 8, 1, 9, 2]
print(my_sort(nums))    # [1, 2, 3, 5, 8, 9]
print(nums)             # [5, 3, 8, 1, 9, 2]   ← 原列表没变 ✓

assert my_sort([3,1,2]) == [1,2,3]
assert my_sort([]) == []
assert my_sort([1]) == [1]
print("检查用例通过")

💡 result[j], result[j+1] = result[j+1], result[j] 是阶段 3 学的"同时赋值",
交换两个值不需要临时变量。

第 4 题答案:

import copy

matrix = [[1, 2], [3, 4]]
backup = copy.deepcopy(matrix)     # ← 浅拷贝不够,要用 deepcopy
backup[0][0] = 999
print(matrix)      # [[1, 2], [3, 4]]   ✓
print(backup)      # [[999, 2], [3, 4]]

为什么 .copy() 不行:它只复制外层列表。
backup[0] 和 matrix[0] 还是同一个内层列表,所以改 backup[0][0] 就改到了 matrix。

第 6 题参考写法:

def safe_append(lst, x):
    new = lst.copy()
    new.append(x)
    return new

orig = [1, 2]
result = safe_append(orig, 3)
assert orig == [1, 2]        # 原列表没被改
assert result == [1, 2, 3]
print("通过")

本阶段自评

  1. 数学:$f''>0$ 对应 ∪ 还是 ∩?在同济约定下叫什么?
  2. 数学:为什么"凹凸"这个词不同教材用法相反?该记什么?
  3. 数学:$f''(x_0)=0$ 能推出 $x_0$ 是拐点吗?举反例。
  4. 数学:三种渐近线分别怎么找?求斜渐近线的快捷方式是什么?
  5. 数学:函数作图六步按什么顺序?一阶导和二阶导各管什么?
  6. 编程:b = a 和 b = a.copy() 有什么本质区别?
  7. 编程:== 和 is 分别比较什么?判断 None 该用哪个?
  8. 编程:浅拷贝和深拷贝的区别?什么情况下浅拷贝会出问题?
  9. 编程:哪些列表方法返回 None?为什么?
  10. 技能:用"标签和盒子"模型解释:为什么 b = a 后改 b 会影响 a?
  11. 连接:"原始数据不可变"这条科研规范,等价于编程里的什么做法?

全部答得上 → 进阶段 8。

最后更新:2026-09-26