Hello 算法 2.3 精讲:时间复杂度,从数清一次执行到理解增长规律
把 n、操作计数、O/Ω/Θ、常见增长阶与最好/最坏/平均情况逐层拆开,用可运行示例推导循环和递归成本,澄清原书的符号简化与 Python 隐藏工作。
阅读主线:时间复杂度不是代码需要几秒
时间复杂度描述:在明确输入规模、输入情况和计算模型后,完成任务的工作量如何随规模增长。它不是运行秒数,也不是代码行数。先把一次执行数清楚,再把不同规模的成本写成函数,最后讨论增长边界。
本节对应原书 2.3.1—2.3.5:增长趋势、渐近上界、推算方法、七类常见增长、最好/最坏/平均情况。下面按阅读依赖重新组织,补充 Python 机制、符号边界与原创例子。已有循环和递归笔记保留原路径,本篇作为复杂度分析总入口,不重复发布那些文章。
文章较长,建议分三次阅读:先读“规模与符号”,再读“循环与递归”,最后读“输入情况与误区”。每段代码独立运行,不依赖前面片段留在解释器里的变量。
知识点 01—02:n 到底是什么,为什么要先说清楚
n 通常表示输入规模,但不是固定含义。查找列表时,n 可以是列表元素个数;处理字符串时,可以是字符个数;处理图时,常常需要顶点数 V 与边数 E 两个量。变量名碰巧叫 n,不足以说明规模。
| 问题 | 规模定义 | 不能混同的量 |
|---|---|---|
| 在列表里找目标 | n = len(values) | 元素值大小不等于元素个数 |
| 比较两批记录的所有配对 | n 条与 m 条 | 精确次数为 nm;未说明 n、m 的关系,不能直接改成单变量平方阶 |
| 处理一个整数 N | 可能按数值 N,也可能按表示位数 b | N 次操作可能是相对 b 的指数级 |
例如正整数 N 的二进制位数 b 满足 2b−1 ≤ N < 2b。如果某算法重复 N 次,写成 Θ(N) 是相对数值的结论;相对位数 b,N 可达到指数级。后续代码中的 n 默认是非负普通整数规模,或由有限列表长度得到;分治示例的规模也是元素数量,不是数据值。
知识点 03:数什么工作,要先选一把尺
可以数比较次数、循环体执行次数、访问多少元素,或把固定的一组动作视为一个单位。不同计数口径的精确公式可能不同,只要基本动作成本有固定上界且不能被忽略,通常可得到一致的增长阶。
本节先采用常用单位成本模型:普通列表索引、固定大小数值的比较和算术等基本操作,按固定成本计;循环体中若复制列表、遍历容器或生成输出,必须展开。计数器的值表示选定的工作次数,不是纳秒,也不声称包含解释器所有指令。
Python 整数支持任意精度,因此极长整数加法、乘法、除法或比较的成本也与位数有关。下面的次数公式仍可准确描述执行了多少次指定动作,但不应无条件升级为任意长整数上的完整位运算复杂度。原书开篇给操作标注纳秒,是教学设定,不是本机测量;本篇不沿用这些数值作性能数据。
机制依据:Python 数值类型:整数精度。实际计时与模型的区别见 2.1 实测与理论估算。
知识点 04:常数阶不等于只做一次,也不等于一定快
下面分别固定执行 17 次、按输入执行 n 次、执行 n×n 次。输出记录的是各自循环体里加 1 的次数。n 改变时,第一个函数的工作不变,第二个成比例增长,第三个按平方增长。
固定次数即使改成一百万,仍与 n 无关;它可能比小输入上的线性循环更慢。“常数阶”强调相对 n 不增长,不保证耗时很小。n=0 时后两个计数为 0,但调用和判断并没有消失,不能据此说实际程序耗时为零。
def fixed_work(n):
count = 0
for _ in range(17):
count += 1
return count
def linear_work(n):
count = 0
for _ in range(n):
count += 1
return count
def square_work(n):
count = 0
for _ in range(n):
for _ in range(n):
count += 1
return count
for n in (0, 1, 4, 8):
print(n, fixed_work(n), linear_work(n), square_work(n))知识点 05:O 是增长上界,不是精确次数
设 T(n) 是已选定口径下的非负成本函数。T(n)=O(f(n)) 表示:可以找到固定正数 c 和一个阈值 n₀,使所有 n ≥ n₀ 都满足 T(n) ≤ c·f(n)。c 不能随 n 任意改变;f(n) 在讨论范围内为正。这里的等号是渐近记法的惯用写法,不是说 O(f(n)) 是一个具体数值。
例如 T(n)=4n+9。n≥1 时 9≤9n,所以 4n+9≤13n。取 c=13、n₀=1,便给出了 O(n) 的有效上界。下表的程序只是核对几个数,不构成对所有 n 的证明;证明来自上面的不等式。
“渐近”表示关注足够大规模以后的增长关系,不要求每一个小输入都满足同一个未经缩放的大小比较。因为 n≤n²(n≥1),线性成本也属于 O(n²),只是这个上界不紧。O 并不自动意味着“恰好同阶”。
for n in (1, 2, 4, 8):
exact = 4 * n + 9
upper = 13 * n
print(n, exact, upper, exact <= upper)知识点 06:Ω 是下界,Θ 是上下都夹住
| 记号 | 最终满足的关系 | 要表达的意思 |
|---|---|---|
| T(n)=O(f(n)) | T(n) ≤ c·f(n) | 渐近上界 |
| T(n)=Ω(f(n)) | T(n) ≥ c·f(n) | 渐近下界 |
| T(n)=Θ(f(n)) | c₁·f(n) ≤ T(n) ≤ c₂·f(n) | 渐近紧确界;增长同阶 |
c、c₁、c₂ 都是固定正数,关系要求在足够大的所有 n 上成立。对 4n+9,n≥1 时有 4n≤4n+9≤13n,因此 T(n)=Θ(n)。它可以写成 O(n²),但不能写成 Θ(n²),因为那会错误要求它至少按某个固定比例的 n² 增长。
这里明确修正原书把 O/Ω/Θ 与三种输入情况一一对应的简化表述。平均复杂度写 O(n) 本身是合法的上界声明;只有同时能给出线性下界,才可加强为 Θ(n),不能见到 O 就直接替换成 Θ。
定义核对:MIT 6.006 渐近复杂度讲义,第 1 页;本文用原创不等式说明。
知识点 07:为什么可以忽略常数系数和低阶项
以 T(n)=3n²+7n+10 为例。n≥1 时,7n≤7n²、10≤10n²,所以 3n²≤T(n)≤20n²,得到 Θ(n²)。不是把真实工作删掉,而是用固定倍数夹住整个函数,保留增长级别。
对于正的固定次数多项式,最高次项最终主导。例如 109n+n² 仍为 Θ(n²),但在 n 远小于 109 的区间里,线性项可能更大。“最终主导”不能替代工程规模下的实测与成本权衡。
也不能说“只要看公式里数字最大的项”:n³ 与 10000n² 的系数大小不是阶数。一般表达式还可能含抵消、分段或振荡,不能把多项式的速记规则当作任意函数定理。指数和阶乘要分别分析,不叫“最高次多项式项”。
知识点 08—09:顺序执行相加,重复执行按实际次数累计
下面为三个阶段打点计数:先记 2 个固定动作,再执行 3n 次动作,最后执行 n² 次动作。总数为 2+3n+n²,而不是把三个阶段全部相乘。
| 结构 | 工作量 | 何时能简写 |
|---|---|---|
| 先处理 A,再处理 B | A(n)+B(n) | 非负成本下取主导增长 |
| 外层 n 次,每轮内层固定 m 次 | n·m | 精确次数 nm 在 m=n 时等于 n²;若 m=Θ(n),增长阶也为 Θ(n²) |
| 第 i 轮内层执行 i 次 | 把各轮次数求和 | 不能未经推导直接乘 n |
| 遇到目标就提前返回 | 取决于结束位置 | 需指定最好、最坏或输入分布 |
这里省略循环控制等细节,只对选定动作精确计数;这些常数倍控制开销不会改变本例的平方增长结论。函数的代码行数不随 n 变化,但循环体执行次数会变化。
def mixed_work(n):
count = 2
for _ in range(3 * n):
count += 1
for _ in range(n):
for _ in range(n):
count += 1
return count
for n in (0, 1, 2, 4):
print(n, mixed_work(n))知识点 10:三种“两层循环”,不能只看外形
第一种计算两个集合的全部索引配对,n 行、每行 m 次,所以 n·m;第二种每一项只与后续项比较,各轮分别是 n−1、n−2、…、0,总数 n(n−1)/2;第三种内层永远只有 5 次,所以总数 5n。
同为两层循环,分别可以是 Θ(nm)、Θ(n²)、Θ(n)。三角形少了一半左右的候选对,但一半是固定倍数,不会把平方增长变成线性。n=0 或 m=0 时计数可为 0,仍有函数和外层控制开销;紧确界说的是相应规模增长的适用区域。
def loop_shapes(n, m):
rectangle = 0
for _ in range(n):
for _ in range(m):
rectangle += 1
triangle = 0
for left in range(n):
for right in range(left + 1, n):
triangle += 1
fixed_inner = 0
for _ in range(n):
for _ in range(5):
fixed_inner += 1
return rectangle, triangle, fixed_inner
for n, m in ((0, 3), (1, 3), (4, 3)):
print(n, m, loop_shapes(n, m))知识点 12—13:每次减半,为什么是对数,而不是 n/2 次
从 16 开始每轮整除 2,状态是 16→8→4→2→1,循环体执行 4 次。n/2 描述第一轮后剩余规模,不是总轮数。忽略取整时,k 轮后剩 n/2k;要到 1,令 2k≈n,所以 k≈log₂n。
下面循环条件是 current>1,n≥1 时精确轮数为 ⌊log₂n⌋;例如 15→7→3→1,只有 3 轮。另一种从 1 开始翻倍、直到不小于 n 的写法,n>1 时轮数是 ⌈log₂n⌉。两者同为 Θ(log n),但精确边界不同。
代码把每轮状态放到列表,只为观察,额外保存了对数数量的状态;若只数轮次,可以不保存列表。这里明确检查 n 的类型和非负范围,避免把负数或浮点除法行为混入定义。
def halving_trace(n):
if type(n) is not int:
raise TypeError("n must be an integer")
if n < 0:
raise ValueError("n must be nonnegative")
states = []
current = n
while current > 1:
states.append(current)
current //= 2
return states
for n in (0, 1, 2, 3, 15, 16):
states = halving_trace(n)
print(n, states, len(states))知识点 14:对数底数何时可以省略
对任何固定且大于 1的底数 a、b,logₐn=logᵦn/logᵦa。分母是与 n 无关的正数,因此这两种对数只差固定倍数,渐近阶相同。
如果底数本身随 n 改变,就不能直接当常数扔掉。例如 logₙn=1(n>1),并不等于通常的对数增长。每轮还必须真的把剩余问题缩到固定比例;仅有“分成多段”的描述不足以判定对数时间。
知识点 15:二分查找怎样把“减半”落实到索引区间
复用第 1 章的二分思想,这里额外返回搜索轮数。输入必须是已按非递减顺序排列的普通整数列表,允许重复,返回任意一个匹配索引或 None;不包含排序准备成本,也不验证有序性。区间 [low, high] 是尚未排除的位置。
每轮取中点:若目标更大,排除中点及左侧,令 low=middle+1;更小则令 high=middle−1。相等直接返回。用索引保留边界,不复制列表。
对 [2,4,6,8,10,12,14] 查找 2:先看索引 3 的 8,区间缩成 [0,2];再看索引 1 的 4,缩成 [0,0];最后索引 0 命中,共 3 轮。查找 8 则只要 1 轮。最长路径 Θ(log n),最好 Θ(1),而非每个目标都恰好 log₂n 轮;空列表为 0 轮。
def binary_search_steps(values, target):
low, high = 0, len(values) - 1
rounds = 0
while low <= high:
rounds += 1
middle = (low + high) // 2
if values[middle] == target:
return middle, rounds
if values[middle] < target:
low = middle + 1
else:
high = middle - 1
return None, rounds
values = [2, 4, 6, 8, 10, 12, 14]
for target in (2, 8, 9):
print(target, binary_search_steps(values, target))
print("empty", binary_search_steps([], 9))知识点 16:n 次任务,每次对数工作,就是 n log n
下面每个 item 都重新把 scale 设为 n,因此每个外层迭代都要完成 ⌊log₂n⌋ 次缩减。n≥1 时,打点数恰好为 n·⌊log₂n⌋,当 n≥2 增长时是 Θ(n log n)。这与前面的共享进度相反:状态被重置了。
对 n=8,外层 8 次,每次 scale 为 8、4、2 三轮,总数 24。n=1 的内层动作虽为 0,外层和调用仍需要工作;不能把计数为 0 当成零运行时间。
def linear_log_work(n):
count = 0
for _ in range(n):
scale = n
while scale > 1:
count += 1
scale //= 2
return count
for n in (0, 1, 2, 4, 8):
print(n, linear_log_work(n))知识点 17:递归要数整棵树,不只看深度
下面构造一个计数实验:规模 n 被分成 ⌊n/2⌋ 和剩余部分,两个子问题都执行;当前层还做 n 次打点。每个规模为 1 的叶子记 1 个单位,空输入记 0。
n=8 时,各层的规模和分别是 8;4+4=8;2+2+2+2=8;八个 1 又是 8。共 4 层,所以打点总数 32。对 n=2k,精确总数为 n(k+1),递归整体 Θ(n log n)。非二次幂时叶子深度可能不同,不能逐层机械照抄相同数值,但平衡划分仍保持同阶。
这只是合并类分治的工作模型,不是排序实现。真实代码中的 for 循环确实逐次打点,返回的是计数;不能把递推式算出的数值大小,直接当作计算该数值所需的时间。递归树与深度的既有讲解可进一步对照。
def split_work(n):
if n <= 1:
return n
left_size = n // 2
count = split_work(left_size) + split_work(n - left_size)
for _ in range(n):
count += 1
return count
for n in (0, 1, 2, 4, 8):
print(n, split_work(n))知识点 18:每层分两支,怎样得到指数增长
若规模每次只减少 1,而每个非基本调用都继续发起两次调用,深度约 n,总调用数便可能是 1+2+4+…+2n=2n+1−1。关键不是“代码有两次函数调用”,而是这些分支确实重复展开,且每层只减少固定规模。
下面故意不缓存相同深度的结果,用两个真实子调用返回叶子数和总调用数。depth=3 时,叶子 8 个、调用 15 次、最长链却只有 4 次同时尚未返回的调用;时间的累计工作与峰值栈深度要分开。规模每次减半的分治树,则可能只有线性数量节点,不能套同一个指数结论。
本节只运行 depth≤4 的展示和小范围验证,不运行大规模穷举。返回值等于 2n本身不是复杂度证明;实际调用展开才是依据。记忆化会改变重复展开方式,需要重新分析,见递归树与记忆化。
def branch_counts(depth):
if type(depth) is not int:
raise TypeError("depth must be an integer")
if depth < 0:
raise ValueError("depth must be nonnegative")
if depth == 0:
return 1, 1
left_leaves, left_calls = branch_counts(depth - 1)
right_leaves, right_calls = branch_counts(depth - 1)
return left_leaves + right_leaves, 1 + left_calls + right_calls
for depth in range(5):
print(depth, branch_counts(depth))知识点 19:n! 个方案,与完整生成所有方案的成本
n 个互不相同元素的排列数为 n!:第一个位置有 n 种选择,下一位置 n−1 种,一直到 1。空集合也有一个空排列,0!=1。下面用 itertools.permutations 生成排列,再为每个排列显式创建长度 n 的列表。
如果只数搜索树的叶子,就是 n! 个;如果真的构造并保存每一条完整排列,输出本身包含 n·n! 个元素位置,因此至少需要 Ω(n·n!) 的输出写入工作。本例完整物化采用 Θ(n·n!) 时间和输出存储(n 增长时、引用与基本控制按常数成本)。教材常说的“阶乘级搜索”,不应让你忽略每个方案的复制与输出成本。
计算数学上的 factorial(n),与枚举 n! 个排列是不同任务。知道答案为 n!,不意味着计算答案也必须执行 n! 次。示例只运行 n≤4;生成器不一次保存所有结果,也不会消除最终遍历所有结果的工作。
from itertools import permutations
def materialized_orders(n):
values = list(range(n))
return [list(order) for order in permutations(values)]
for n in range(5):
orders = materialized_orders(n)
output_slots = sum(len(order) for order in orders)
print(n, len(orders), output_slots)知识点 20:七种常见增长,怎样对照理解
| 增长类 | 典型工作结构 | n 翻倍后主导项约变为 |
|---|---|---|
| Θ(1) 常数 | 固定数量基本动作 | 不变 |
| Θ(log n) 对数 | 每次缩小到固定比例,只继续一个子问题 | 增加一个固定量(固定底数) |
| Θ(n) 线性 | 逐项执行固定成本动作 | 2 倍 |
| Θ(n log n) 线性对数 | n 次对数工作;平衡分治每层线性工作 | 略多于 2 倍,比例趋近 2 |
| Θ(n²) 平方 | 所有索引对或两个随 n 线性增长的完整层 | 4 倍 |
| Θ(2ⁿ) 指数 | 两分支展开 n 层且无共享缓存 | 从 2ⁿ 变为 2²ⁿ,不是 2 倍 |
| 阶乘类 | n、n−1、…、1 层层选择 | 增速更快;还需计每个完整输出的成本 |
这是主导增长形态的对照,不是任意小规模上的固定耗时排名。多项式也不只有平方:n³、n⁴ 等同样可能出现;指数底数 2 只是示例。比较实际方案时,不得跳过输入、正确性、内存和常数成本。
知识点 21—22:最好与最坏,是同一规模下选不同输入
用线性查找观察。输入是有限普通整数列表,返回第一次匹配位置与比较次数;未找到返回 None。第 k 个位置命中(从 1 开始计),就比较 k 次;完全没有目标则比较 n 次。
对长度为 n≥1 的合法输入,最好情况是第一项命中,比较一次;最坏情况是最后一项命中或不存在目标,比较 n 次。故最好成本为 Θ(1),最坏为 Θ(n)。这是先选择输入情况,再描述该成本函数的增长,不是 O/Ω 决定该看哪个输入。
下列四次都用同一个函数:目标 8 在头部、目标 6 在中间、目标 2 在末尾、目标 99 不存在。索引 0 是成功位置,不要用 if position 当作找到的判断。
def find_value_steps(values, target):
comparisons = 0
for index, value in enumerate(values):
comparisons += 1
if value == target:
return index, comparisons
return None, comparisons
values = [8, 4, 6, 2]
for target in (8, 6, 2, 99):
print(target, find_value_steps(values, target))
print("empty", find_value_steps([], 99))知识点 23:平均情况不是“最好和最坏取平均”
平均成本是某个明确输入分布下的数学期望。若目标保证存在且唯一,并且出现于每个位置的概率均为 1/n,则比较次数 1、2、…、n 的概率相同,期望为 (1+2+…+n)/n=(n+1)/2,因此平均为 Θ(n)。
这里精确值是 (n+1)/2,不是 n/2;两者增长同阶,但说精确次数时不能少掉半次。若目标总在头部,期望为 1;若目标缺失概率为 p,命中时仍均匀,期望为 p·n+(1−p)·(n+1)/2。分布不同,平均成本可以不同。
下面对 n=4 的全部 24 种排列进行穷举,计算查找 0 的比较次数;这不是随机抽几个样本。计数器输出最少、最多和精确平均值。运行同一个输入七次测出的耗时中位数,并不是这里对输入分布求得的平均复杂度。
from fractions import Fraction
from itertools import permutations
def comparisons_to_zero(values):
comparisons = 0
for value in values:
comparisons += 1
if value == 0:
return comparisons
return comparisons
costs = [comparisons_to_zero(values) for values in permutations(range(4))]
print(len(costs), min(costs), max(costs), Fraction(sum(costs), len(costs)))知识点 26:局部很快,不代表整个流程同样快
二分定位是对数工作,不代表“定位后插入列表”也是对数工作。Python 官方 bisect 文档明确:insort 的对数搜索之后,还有线性插入,因此整体为 O(n)。要分析的是完整任务边界,而不是只挑其中最快的一步。
同理,若每次查询前重新排序,排序工作必须计入该次任务;若先排序一次后查询很多次,可以分别报告准备成本、每次查询成本和总成本。已排序输入上的查找复杂度,不能悄悄替换成任意输入的端到端复杂度。
最好/最坏/平均、期望与摊还也不是同义词。摊还分析考察一串操作的总成本如何分摊,不等于随机输入的平均;后续列表扩容章节会再展开。阅读“常数时间操作”时,要确认它指最坏、期望还是摊还成本。
分析一段新代码:按这六问复查
- 任务与规模:要求什么输出?n、m 指什么?输入是否合法、有序、可重复吗?
- 成本口径:统计比较、访问、复制还是完整运行?哪些操作可以按固定成本处理?
- 执行路径:哪些循环重置,哪些共享进度?是否提前返回?递归分几支、规模如何变化?
- 累计公式:先后相加、重复求和;只有条件成立时才简写成乘积。
- 输入情况与记号:先定义最好/最坏/分布期望,再给 O、Ω 或 Θ,避免混层。
- 反例与证据:空输入、重复、命中位置、非二次幂和隐藏操作是否改变结论?用计数核对小输入,通式仍给数学理由。
有限样例可以发现推导错误,但不能证明所有规模的渐近关系;单次跑分也不能证明复杂度。若计数和公式冲突,先定位退出条件、取整、是否重复计算或单次动作中隐藏的工作,不先改公式凑答案。
本节复盘:10 道建议题与参考答案
- “T(n)=O(n²)”是否意味着恰好平方增长?给一个反例。
- 用固定上下界证明 4n+9=Θ(n)。
- 先执行 n 次循环,再执行 n² 次,为什么不是 n³?
- 外层 n 次,内层每次固定 5 次,复杂度是什么?
- 16 每轮整除 2、条件大于 1,共几轮?15 呢?
- 对数底数随 n 变化,还能直接省略吗?
- 深度 n 的两分支、每支只减 1,为什么总调用数不是 2n?
- 求 n! 的值与生成所有排列,是否同一个任务?
- 目标存在且均匀出现,线性查找的期望比较数是多少?目标总在首位呢?
- 只有一层 for,循环体每次复制长度递增的前缀,为什么会平方?
1—4:符号与循环
1. 不是;T(n)=n 也属于 O(n²),但不是 Θ(n²)。2. n≥1 时 4n≤4n+9≤13n。3. 两阶段顺序执行,次数相加 n+n²,取主导项得到 Θ(n²);没有让每个外层动作都重复整段平方工作。4. 总计 5n 个内层动作,单位成本下 Θ(n)。
5—8:对数、递归与输出
5. 分别为 4 和 3 轮,取整和退出条件决定精确值。6. 不能直接省略;例如 logₙn=1。7. 前面每层的所有节点都继续分裂,总数是 1+2+…+2ⁿ=2ⁿ⁺¹−1。8. 不同;完整物化 n! 条长度 n 的结果,输出本身就需要 n·n! 个位置。
9—10:分布与隐藏工作
9. (n+1)/2;总在首位时为 1,所以必须指定分布。10. 累计复制 1+2+…+n=n(n+1)/2 个位置,一行切片内部仍有工作。
这些题目是作者建议,未记作你已提问、已完成、已复盘或掌握。可先手算,再运行小输入;有新追问时优先补到相应章节。
来源、模型说明与下一节
主题依据:krahets / 《Hello 算法》2.3 时间复杂度,2026-10-16 核对,覆盖 2.3.1—2.3.5。本篇重新组织讲解,加入原创计数代码、状态表、上下界证明、输入分布、Python 实现边界与练习;对原书“O/Ω/Θ 对应三种输入情况”及平均比较次数的简化作了明确澄清。
相关改编文字采用 CC BY-NC-SA 4.0,保留署名、来源与改动说明,遵守非商业及相同方式共享;不改变本站其他内容或源码许可。渐近定义另核对 MIT 6.006 讲义;数学例子与代码均为本文编写。
Python 机制参考官方 内置类型、itertools.permutations、bisect 性能说明和列表切片。本节不通过计时测量推断增长阶;示例验证记录保存在源码中。
现已发布 2.4 空间复杂度精讲,重点区分辅助空间、输入/输出、递归栈和峰值。其余条目以全书覆盖表为准。
发布前验证环境为 CPython 3.12.14:14 段代码逐段独立运行并核对标准输出;检查 1,089 组计数参数、4,097 个减半输入、11 个分支深度与 10 个明确拒绝的非法输入;用独立答案核对 5,465 组线性查找和 588 组二分查找,再穷举 n=1…7 的 5,913 个排列验证平均比较数。另检查 n=0…7 的排列输出数量、唯一性与完整元素位置数。这些有限验证支持示例正确性,不替代正文的渐近推导。