跳转至

09 推导与实现练习

只读算法说明很容易产生“我懂了”的错觉。本章练习不以上板观察数据为重点,而是训练三件事:能推导关键公式,能手算一小段数据,能把公式写成注释清楚的代码。

练习 1:滑动平均的递推公式

目标:从普通平均推到递推平均。

步骤:

  1. 写出 N 点平均公式:y[n] = (x[n] + ... + x[n-N+1]) / N
  2. 写出上一时刻公式:y[n-1] = (x[n-1] + ... + x[n-N]) / N
  3. 两式相减,得到 sum[n] = sum[n-1] + x[n] - x[n-N]
  4. 用数组 {10, 12, 11, 100, 12} 和窗口 N = 3 手算输出。
  5. 写出带注释的环形缓冲区代码。

思考:为什么滑动平均必须保存“最旧的那个样本”?

练习 2:滑动平均的截止频率

目标:把“平均多少次”改成“设计多低的截止频率”。

步骤:

  1. 设采样率 fs = 1000 Hz,窗口长度 N = 20
  2. 计算第一零点:f_zero1 = fs / N
  3. 用近似公式计算 -3 dB 截止频率:fc ≈ 0.443 * fs / N
  4. 计算群延迟:delay = (N - 1) / (2 * fs)
  5. 说明为什么这个平均滤波对 50 Hz 正弦干扰特别有效。

思考:如果目标不是压制 50 Hz,而是希望 fc ≈ 10 HzN 应该大约取多少?

练习 3:一阶滞后的公式含义

目标:理解 y[n] = y[n-1] + alpha * (x[n] - y[n-1])

步骤:

  1. 把公式改写为 y[n] = (1-alpha)y[n-1] + alpha x[n]
  2. 说明为什么两个权重加起来等于 1。
  3. alpha = 0.2,输入从 0 跳到 100,手算前 5 次输出。
  4. 写出带初始化逻辑的 C 函数。

思考:alpha 越小,为什么输出越慢?

练习 4:一阶滞后的 alpha 设计

目标:从目标截止频率反推 alpha

步骤:

  1. 设采样率 fs = 200 Hz,目标截止频率 fc = 5 Hz
  2. 用近似式计算:alpha ≈ 2*pi*fc/fs
  3. 用匹配式计算:alpha = 1 - exp(-2*pi*fc/fs)
  4. 比较两个结果,说明为什么 fc/fs 较小时它们接近。
  5. 估算低频群延迟:delay_samples ≈ (1-alpha)/alpha

思考:同样是“看起来平滑”,为什么给出 fc 比给出 alpha 更专业?

练习 5:尖峰干扰处理

目标:比较平均、中值、限幅平均对尖峰的处理差异。

构造数据:

100, 101, 99, 100, 500, 101, 100, 99

分别手算普通平均、3 点中值、限幅后一阶滞后。

思考:为什么普通平均会被单个 500 明显拉偏?

练习 6:迟滞报警

目标:实现一个稳定的温度报警。

要求:

  • 高于 60 摄氏度报警。
  • 低于 55 摄氏度解除。
  • 在 55 到 60 之间保持原状态。

思考:如果只用 60 一个阈值,会发生什么?

练习 7:一维卡尔曼增益

目标:从“不确定度”理解卡尔曼增益。

步骤:

  1. 写出增益公式:K = P / (P + R)
  2. 分别计算 P = 1, R = 9P = 9, R = 1 时的 K
  3. 解释 K 大时为什么更相信测量。
  4. 写出一维卡尔曼更新函数,并给每行加注释。

思考:什么时候滤波器更相信测量?什么时候更相信预测?

练习 8:Goertzel 的递推结构

目标:理解 Goertzel 为什么只需要保存两个历史状态。

步骤:

  1. 写出递推式:s[n] = x[n] + coeff*s[n-1] - s[n-2]
  2. 标出 s[n-1]s[n-2] 在代码中的变量。
  3. 说明为什么每次循环后要把 s1 移到 s2,再把 s0 移到 s1
  4. 手算 4 个样本,确认状态更新顺序没有写反。

思考:窗口长度为什么会影响频率分辨率?