09 推导与实现练习
只读算法说明很容易产生“我懂了”的错觉。本章练习不以上板观察数据为重点,而是训练三件事:能推导关键公式,能手算一小段数据,能把公式写成注释清楚的代码。
练习 1:滑动平均的递推公式
目标:从普通平均推到递推平均。
步骤:
- 写出
N点平均公式:y[n] = (x[n] + ... + x[n-N+1]) / N。 - 写出上一时刻公式:
y[n-1] = (x[n-1] + ... + x[n-N]) / N。 - 两式相减,得到
sum[n] = sum[n-1] + x[n] - x[n-N]。 - 用数组
{10, 12, 11, 100, 12}和窗口N = 3手算输出。 - 写出带注释的环形缓冲区代码。
思考:为什么滑动平均必须保存“最旧的那个样本”?
练习 2:滑动平均的截止频率
目标:把“平均多少次”改成“设计多低的截止频率”。
步骤:
- 设采样率
fs = 1000 Hz,窗口长度N = 20。 - 计算第一零点:
f_zero1 = fs / N。 - 用近似公式计算
-3 dB截止频率:fc ≈ 0.443 * fs / N。 - 计算群延迟:
delay = (N - 1) / (2 * fs)。 - 说明为什么这个平均滤波对 50 Hz 正弦干扰特别有效。
思考:如果目标不是压制 50 Hz,而是希望 fc ≈ 10 Hz,N 应该大约取多少?
练习 3:一阶滞后的公式含义
目标:理解 y[n] = y[n-1] + alpha * (x[n] - y[n-1])。
步骤:
- 把公式改写为
y[n] = (1-alpha)y[n-1] + alpha x[n]。 - 说明为什么两个权重加起来等于 1。
- 令
alpha = 0.2,输入从 0 跳到 100,手算前 5 次输出。 - 写出带初始化逻辑的 C 函数。
思考:alpha 越小,为什么输出越慢?
练习 4:一阶滞后的 alpha 设计
目标:从目标截止频率反推 alpha。
步骤:
- 设采样率
fs = 200 Hz,目标截止频率fc = 5 Hz。 - 用近似式计算:
alpha ≈ 2*pi*fc/fs。 - 用匹配式计算:
alpha = 1 - exp(-2*pi*fc/fs)。 - 比较两个结果,说明为什么
fc/fs较小时它们接近。 - 估算低频群延迟:
delay_samples ≈ (1-alpha)/alpha。
思考:同样是“看起来平滑”,为什么给出 fc 比给出 alpha 更专业?
练习 5:尖峰干扰处理
目标:比较平均、中值、限幅平均对尖峰的处理差异。
构造数据:
分别手算普通平均、3 点中值、限幅后一阶滞后。
思考:为什么普通平均会被单个 500 明显拉偏?
练习 6:迟滞报警
目标:实现一个稳定的温度报警。
要求:
- 高于 60 摄氏度报警。
- 低于 55 摄氏度解除。
- 在 55 到 60 之间保持原状态。
思考:如果只用 60 一个阈值,会发生什么?
练习 7:一维卡尔曼增益
目标:从“不确定度”理解卡尔曼增益。
步骤:
- 写出增益公式:
K = P / (P + R)。 - 分别计算
P = 1, R = 9和P = 9, R = 1时的K。 - 解释
K大时为什么更相信测量。 - 写出一维卡尔曼更新函数,并给每行加注释。
思考:什么时候滤波器更相信测量?什么时候更相信预测?
练习 8:Goertzel 的递推结构
目标:理解 Goertzel 为什么只需要保存两个历史状态。
步骤:
- 写出递推式:
s[n] = x[n] + coeff*s[n-1] - s[n-2]。 - 标出
s[n-1]和s[n-2]在代码中的变量。 - 说明为什么每次循环后要把
s1移到s2,再把s0移到s1。 - 手算 4 个样本,确认状态更新顺序没有写反。
思考:窗口长度为什么会影响频率分辨率?