4.2 数值计算
知识点
1.什么是"数值计算"
数值计算:以数值为处理对象,通过算术运算得到数值结果的问题。
典型特征:
- 输入是数,输出也是数;
- 可以用数学公式描述;
- 常常需要近似(很多问题的精确解求不出来)。
本小节要掌握的四种策略:
| 策略 | 一句话 | 典型问题 |
|---|---|---|
| 解析法 | 套公式直接算 | 圆面积、一元二次方程 |
| 迭代法 | 反复逼近,越来越接近答案 | 求平方根、二分法求根、求 π |
| 递推法 | 用前一项推后一项 | 斐波那契、累加、累乘 |
| 穷举法 | 全试一遍,符合条件的留下 | 百钱百鸡、水仙花数、素数判断 |
选择思路:有公式用解析 → 没公式用迭代 → 有前后关系用递推 → 实在不行就穷举。
2.解析法:套用公式
思路:问题已经有明确的数学公式,直接代入计算即可。
关键:把公式准确翻译成表达式,注意运算优先级与单位换算。
示例 1:圆面积与周长
import math
r = float(input("半径:"))
area = math.pi * r ** 2 # πr²
perimeter = 2 * math.pi * r # 2πr
print(f"面积 {area:.2f},周长 {perimeter:.2f}")
示例 2:一元二次方程求根
import math
a, b, c = 1, -3, 2 # 对应 x² - 3x + 2 = 0
delta = b ** 2 - 4 * a * c # 判别式
if delta > 0:
x1 = (-b + math.sqrt(delta)) / (2 * a)
x2 = (-b - math.sqrt(delta)) / (2 * a)
print(x1, x2) # 2.0 1.0
elif delta == 0:
print(-b / (2 * a))
else:
print("无实数根")
要点:
- 先判断判别式,再求根(否则
math.sqrt遇负数报错); - 分母不能为 0(
a != 0是一元二次方程的前提); - 除法用
/(真除法),注意括号:(-b + ...) / (2*a)不能漏括号。
示例 3:单位换算与复合公式
# 摄氏温度转华氏温度:F = C × 9/5 + 32
c = float(input("摄氏温度:"))
f = c * 9 / 5 + 32
print(f"{c}℃ = {f:.1f}℉")
解析法的局限:很多问题没有公式(如求 sin(37°) 的值、求任意方程的根),这时就要迭代。
3.迭代法:用旧值逼近新值
(1)什么是迭代
核心思想:先给一个初始猜测值,然后用同一个公式不断算出更好的近似值,直到满足精度要求为止。
迭代的三要素:
- 初始值(猜一个);
- 迭代公式(怎么变好);
- 终止条件(什么时候够好了)。
标准模板:
x = 初始猜测值
while abs(新值 - 旧值) > 精度: # 终止条件
旧值 = 新值
新值 = 迭代公式(旧值)
结果 = 新值
精度通常取 1e-6(即 0.000001,1e-6 是科学计数法写法)。
(2)二分法求方程的根(经典,必会)
问题:求 f(x) = x³ - 2x - 5 = 0 在区间 [2, 3] 内的根。
原理:若 f(a) 与 f(b) 异号(一正一负),则区间内必有根。取中点 m,看根在哪一半,不断把区间折半。
# 4-1 二分法求 x³ - 2x - 5 = 0 在 [2,3] 内的根
def f(x):
return x ** 3 - 2 * x - 5
a, b = 2.0, 3.0
while b - a > 1e-6: # 区间足够小就停
m = (a + b) / 2
if f(a) * f(m) <= 0: # 根在左半区间
b = m
else: # 根在右半区间
a = m
print(f"根约为 {(a + b) / 2:.6f}") # 约 2.094551
要点:
- 每次区间减半,所以复杂度是 O(log n)——100 次二分可把区间缩小到 10⁻³⁰ 量级;
- 必须保证初始区间两端异号,否则不保证有根;
- 教材常以"猜数字(1~100)"作为二分法的直观例子:每次猜中间值,7 次必中。
(3)牛顿迭代法求平方根
原理:求 √a 等价于解 x² - a = 0。牛顿迭代公式:
x(n+1) = (x(n) + a / x(n)) / 2
# 4-2 用牛顿迭代法求平方根
def my_sqrt(a):
x = a / 2 # 初始猜测
while abs(x * x - a) > 1e-9: # 精度控制
x = (x + a / x) / 2
return x
print(f"{my_sqrt(2):.10f}") # 1.4142135624
优点:收敛极快(每步有效数字大约翻倍),一般 5~6 次就够; 缺点:对初始值敏感,初值太离谱可能不收敛。
(4)用级数求 π
莱布尼茨级数:
π/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9 - …
# 4-3 用级数求 π(交错求和)
pi_4 = 0
sign = 1
for i in range(1, 1000000, 2): # 分母 1,3,5,…
pi_4 += sign / i
sign = -sign # 正负号交替
print(f"π ≈ {pi_4 * 4:.6f}")
要点:正负号交替用 sign = -sign 实现;精度随项数增加而提高,但收敛很慢(100 万项才 6 位左右)——这正说明"算法效率"的重要。
另一种经典方法:蒙特卡洛法(随机投点)
import random
n = 100000
hit = 0
for _ in range(n):
x, y = random.random(), random.random()
if x * x + y * y <= 1: # 落在 1/4 圆内
hit += 1
print(f"π ≈ {4 * hit / n:.4f}") # 面积比 = π/4
思想:用频率估计概率,点越多越接近 π。这是"用随机模拟解决确定问题"的典型。
4.递推法:由前一项推出后一项
定义:已知初始项,通过递推关系逐项算出后面的项。
与迭代的关系:递推是迭代的一种具体形式(按序号一步步推)。
(1)累加与累乘(最基础的递推)
# 求 1+2+…+100
s = 0
for i in range(1, 101):
s = s + i # 递推关系:S(n) = S(n-1) + n
# 求 10!
f = 1
for i in range(1, 11):
f = f * i # 递推关系:F(n) = F(n-1) × n
(2)斐波那契数列
定义:F(1) = 1, F(2) = 1, F(n) = F(n-1) + F(n-2)(n ≥ 3)。
# 4-4 输出前 20 项斐波那契数
a, b = 1, 1
for i in range(20):
print(a, end=" ")
a, b = b, a + b # 两变量滚动,省去数组
要点:用两个变量"滚动",不需要保存整个数列(空间 O(1));这体现了"用少量变量实现递推"的技巧。
注意:斐波那契增长极快,第 90 项左右就超出普通整数范围(Python 支持大整数,但其他语言可能溢出)。
(3)辗转相除法求最大公约数
# 4-5 辗转相除
def gcd(m, n):
while n != 0:
m, n = n, m % n # 递推:gcd(m,n) = gcd(n, m%n)
return m
print(gcd(48, 18)) # 6
原理:m 与 n 的最大公约数等于 n 与 m % n 的最大公约数,余数为 0 时除数即为答案。
5.穷举法(枚举法)
思路:把所有可能的情况列举出来,逐个检验,满足条件的输出。
适用条件:
- 解的范围有限且可枚举;
- 有明确的检验条件。
(1)水仙花数
# 4-6 三位数中各位数字立方和等于本身的数
for n in range(100, 1000):
a = n // 100 # 百位
b = n // 10 % 10 # 十位
c = n % 10 # 个位
if a**3 + b**3 + c**3 == n:
print(n, end=" ") # 153 370 371 407
(2)百钱百鸡
# 4-7 公鸡 5 元、母鸡 3 元、小鸡 1 元 3 只,100 元买 100 只
for x in range(0, 21): # 公鸡最多 20 只
for y in range(0, 34): # 母鸡最多 33 只
z = 100 - x - y # 小鸡数量由总数推出,省一层循环
if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100:
print(f"公鸡{x}只,母鸡{y}只,小鸡{z}只")
穷举的两条优化法则(重要):
- 缩小枚举范围——用总数量、总金额等约束算出上界(公鸡不会超过 20 只);
- 减少循环层数——用"总数 - 已知部分"直接算出未知量,把三重循环降为两重,效率提升几十倍。
(3)判断素数
# 4-8 判断素数(优化版)
n = int(input("n = "))
is_prime = True
if n < 2:
is_prime = False
else:
for i in range(2, int(n ** 0.5) + 1): # 只需试到 √n
if n % i == 0:
is_prime = False
break # 找到因数立即结束
print("是素数" if is_prime else "不是素数")
两个优化点:
- 只需试到
√n——因为若n = a × b,则a、b中必有一个不超过√n; - 找到因数就
break——不必试完。
效率对比:判断 1000003 是否素数,试到 √n 约 1000 次,而试到 n-1 要 100 万次——相差 1000 倍。
6.递归:自己调用自己(拓展)
定义:函数直接或间接地调用自身,把问题转化为规模更小的同类问题。
两要素:
- 终止条件(边界)——最小的、已知答案的情况;
- 递归关系——大问题如何化成小问题。
# 4-9 递归求阶乘
def fact(n):
if n == 1: # 终止条件
return 1
return n * fact(n - 1) # 递归关系
print(fact(5)) # 120
递归与递推的对比:
| 对比 | 递推(循环) | 递归 |
|---|---|---|
| 方向 | 从已知到未知,正向推 | 从大到小,层层深入再逐层返回 |
| 代码 | 稍长 | 短,贴近数学定义 |
| 效率 | 高 | 有函数调用开销,可能重复计算 |
| 空间 | 通常 O(1) | 与递归深度成正比 |
| 适用 | 顺序明确的累加、数列 | 具有自相似结构的问题(汉诺塔、树的遍历) |
结论:能用循环就用循环;结构天然递归(如汉诺塔、目录遍历)时用递归更简洁。
递归的陷阱:没有终止条件或终止条件永远达不到,会报 RecursionError: maximum recursion depth exceeded。
7.实现细节与易错点
| 序号 | 易错点 | 说明 | 正确做法 |
|---|---|---|---|
| 1 | 精度写成 1e6 | 那是 100 万,不是 0.000001 | 1e-6 |
| 2 | 用 == 判断两个浮点数相等 | 浮点误差导致永远不成立 | abs(a - b) < 1e-9 |
| 3 | 二分法初始区间两端同号 | 区间内可能无根,结果错 | 先验证 f(a) * f(b) <= 0 |
| 4 | 迭代不更新变量 | 死循环 | 循环体内必须有 x = 新值 |
| 5 | 穷举范围开太大 | 运行极慢甚至超时 | 用约束缩小范围 |
| 6 | math.sqrt 参数为负 | ValueError: math domain error | 先判断非负 |
| 7 | 除法分母为 0 | ZeroDivisionError | 先判断 |
| 8 | 整数除法用了 / 得到小数 | 结果类型不符 | 需要整数时用 // |
| 9 | 递归无终止条件 | RecursionError | 先写边界 |
| 10 | 级数求 π 忘记交错符号 | 结果偏离很大 | sign = -sign 交替 |
精度控制的三种常见写法(等价,任选):
while abs(x_new - x_old) > 1e-6: # 相邻两次差小于精度
while abs(f(x)) > 1e-6: # 函数值足够接近 0
while b - a > 1e-6: # 区间足够小
8.速查表
| 序号 | 考点 | 结论 |
|---|---|---|
| 1 | 四种数值计算策略 | 解析、迭代、递推、穷举 |
| 2 | 解析法 | 套公式直接算 |
| 3 | 解析法前提 | 有明确的数学公式 |
| 4 | 一元二次方程先算 | 判别式 Δ = b²-4ac |
| 5 | 迭代三要素 | 初始值、迭代公式、终止条件 |
| 6 | 常用精度 | 1e-6(0.000001) |
| 7 | 二分法前提 | 区间两端函数值异号 |
| 8 | 二分法复杂度 | O(log n)(每次减半) |
| 9 | 牛顿迭代求平方根公式 | x = (x + a/x) / 2 |
| 10 | 级数求 π 的符号 | 正负交替(sign = -sign) |
| 11 | 蒙特卡洛求 π 的原理 | 面积比 = π/4,用频率估计概率 |
| 12 | 递推 | 由前项推后项 |
| 13 | 累加器初值 | 0 |
| 14 | 累乘器初值 | 1 |
| 15 | 斐波那契递推式 | F(n)=F(n-1)+F(n-2),前两项为 1 |
| 16 | 斐波那契实现技巧 | 两个变量滚动,空间 O(1) |
| 17 | 辗转相除 | gcd(m,n)=gcd(n,m%n),余 0 时除数即答案 |
| 18 | 穷举两法则 | 缩小范围 + 减少循环层数 |
| 19 | 判断素数优化 | 试到 √n,找到因数 break |
| 20 | 水仙花数范围 | 100~999(三位数) |
| 21 | 递归两要素 | 终止条件 + 递归关系 |
| 22 | 递归无终止的报错 | RecursionError |
| 23 | 递归空间复杂度 | 与递归深度成正比 |
| 24 | 浮点比较 | 用 abs(a-b) < 1e-9 |
9.课堂提示(讲课参考)
- "猜数字"引出二分法:先让学生玩 1~100 猜数字,问"最少几次必中",答案是 7 次(2⁷=128)——log 的概念顺势讲出。
- 牛顿迭代的"神奇"现场演示:打印每次迭代的
x值,学生看到 1.5 → 1.4167 → 1.414215…三步就精确到 6 位,会很有触动。 - 穷举优化的对比实验:百钱百鸡先写三重循环(约 100 万次),再改成两重(约 700 次),用计时器显示时间差。
- 精度写成
1e6的坑要让学生踩一次:程序跑很久不结束,再解释1e-6与1e6的差别。 - 递推与递归用斐波那契对照讲:同一数列两种写法,比较代码长度与执行效率,再引到 4.3 汉诺塔"非递归不可"的场景。