简介:这份《信号与系统课后答案》PDF面向电子工程、通信工程等专业学习“信号与系统分析”课程的学生,尤其适合南京邮电大学相关课程的学习者用于课后习题核对、周期性与抽样等概念梳理。压缩包内仅含1个PDF文件,大小约1.12MB,目录围绕第一章信号与系统的基本概念展开,涵盖连续周期信号判断与基本周期求解、离散周期序列识别及最小公倍数周期计算、两个周期信号相加仍为周期信号的条件证明、连续信号均匀抽样后数字角频率与序列周期的分析,以及能量信号、功率信号与非能非功信号的判定。文档保留了较完整的解题过程、定义式推导和波形说明,便于读者对照课堂进度查漏补缺,理解周期性、抽样与信号分类之间的关联。目前已有3934人学习,适合作为阶段性复习和习题自检的参考材料。
1. 课后答案 PDF 只给你结果,验证链才给你过程
信号与系统这门课的课后答案,几乎每个学过的人手里都有一份。真到复习或者考研二轮刷题的时候,翻两次就发现它不好用:一道 20 分的卷积题,答案常常只写「y(t) = e^{-t} - e^{-2t},t ≥ 0」,中间两页推导被压缩成一行,你错在哪里完全看不出来。更麻烦的是符号约定,有的写 jω,有的写 iω,单边拉普拉斯和双边混着用,离散题的 n 起点从 0 还是从 1 开始也不统一,直接照抄答案反而会把错误的过程固化下来。
我后来换了个思路:把课后答案 PDF 当成一套测试用例集,而不是教材。每道题的最终结果是一个 assert,我要做的是自己求出解析解、用数值方法复现一遍、再用频域或时域的另一条路径交叉验证,三条链都过了才算这道题做对。这个做法对本科生复习、考研二轮、以及工作后要补离散时间系统前置知识的人都适用,代价是一道题多花五到十分钟,收益是你能定位到自己到底错在哪一步,而不是对着答案发呆。
2. 一道卷积题拆成三条独立的验证链
2.1 先把解析解推到能写进代码的形式
拿最经典的连续卷积举例:x(t) = e^{-t}u(t),h(t) = e^{-2t}u(t),求 y(t) = x(t) * h(t)。手算得到 y(t) = (e^{-t} - e^{-2t})u(t)。很多人算到这一步就停了,把结果和答案对一下了事。但「能写进代码的形式」要求更高:你得明确 t 的定义域、明确 u(t) 在 t=0 处的取值约定、明确结果在 t→∞ 时的衰减速度。
这里有个容易被忽略的点:解析解的有效区间。卷积结果在 t < 0 时为 0,是因为两个信号都从 0 开始;如果换成 x(t) = u(t) - u(t-2),y(t) 的表达式就要分成 0 ≤ t < 2 和 t ≥ 2 两段,答案 PDF 里经常只写第二段,照着抄必翻车。我一般会把分段条件先在纸上写清楚,再动手敲代码,否则数值验证的时候会被边界那一点误差折磨很久。
还有一个习惯:先把解在几个特殊点上手算一遍,比如 t=0、t=ln2(这里是峰值)、t=1、t=5。t=0 时 y=0,t=ln2 时 y=0.25,t=1 时 y≈0.2326。这几个点后面拿来核对数值结果,比看曲线形状可靠得多。
2.2 数值卷积只是矩形积分的近似,步长怎么定
连续卷积的定义是积分,代码里能做的只有离散求和。最朴素的近似是把积分拆成矩形:
import numpy as np dt = 1e-4 # 采样步长,取最快时间常数的 1/100 t = np.arange(0, 6, dt) # 6 个时间单位,足够看到衰减尾巴 x = np.exp(-t) # x(t) = e^{-t} u(t),t >= 0 h = np.exp(-2 * t) # h(t) = e^{-2t} u(t) y_num = np.convolve(x, h) * dt # 离散卷积补偿 dt 才能逼近积分 t_num = np.arange(len(y_num)) * dt # 结果长度 = len(x) + len(h) - 1 y_ref = np.exp(-t_num) - np.exp(-2 * t_num) # 解析解 mask = t_num < 6 err = np.max(np.abs(y_num[mask] - y_ref[mask])) print("max abs error:", err)这段代码跑出来的绝对误差在 1e-4 量级,正好和 dt 同阶,说明离散近似本身收敛正常;如果误差跑到 1e-2 甚至更大,那一定不是精度问题,而是某个参数写错了。
关键参数只有两个。dt 决定积分近似的精度,经验值是取系统最快时间常数的 1/50 到 1/100,这里最快时间常数是 e^{-2t} 对应的 0.5,所以 dt = 1e-4 相当于 1/5000,够用且不慢。t 的终点取最长衰减的 5 到 7 倍,e^{-t} 在 t=6 时已经衰到 0.0025,再往后算只是浪费内存。
还有一个坑:np.convolve 默认是「full」模式,结果长度是 2N-1,很多人直接拿它和长度 N 的参考信号相减,numpy 会广播报错或者悄悄错位。正确做法是用 t_num 重新生成参考信号,保证两边长度一致,再截取有效区间比较。
2.3 频域相乘做第三条链:量纲与对称性检查
时域验证过了还不够,因为时域近似和解析解可能同时错在同一个地方(比如都把 u(t) 的起点弄错了)。第三条链是走频域:卷积在频域是相乘,如果 Y(jω) = X(jω)H(jω) 也对得上,那基本可以放心。
用 DFT 近似连续傅里叶变换时,有一个必须补偿的因子:X[k] ≈ dt · Σ x[n]e^{-j2πkn/N},频率轴是 f_k = k/(N·dt),ω = 2πf。
import numpy as np a = 1.0 dt = 1e-4 N = 2 ** 18 # 总时长 N*dt ≈ 26.2 s,保证截断误差可忽略 t = np.arange(N) * dt x = np.exp(-a * t) X = np.fft.fft(x) * dt # 乘 dt 才是积分近似 f = np.fft.fftfreq(N, d=dt) w = 2 * np.pi * f X_ref = 1 / (a + 1j * w) # 解析的傅里叶变换 sel = (f >= 0) & (f < 5.0) # 只信低频段,高频段受离散化影响大 rel = np.abs(X[sel] - X_ref[sel]) / np.abs(X_ref[sel]) print("max rel err:", rel.max())三个参数控制着这条链的质量。N·dt 决定截断误差,要让 e^{-a·N·dt} 小到可忽略,26 秒时 e^{-26} ≈ 5e-12,随便用;dt 决定矩形积分的相位误差,量级是 O(ω·dt),所以只在 f < 5 Hz(ω < 31.4)的范围内比较,相对误差能压到 1e-3 以内;频率比较窗口的选择本质上是在「信噪比」和「适用频段」之间取折中,超过 1/(2dt) 的奈奎斯特频率之后,DFT 结果和连续变换没有可比性。
我通常会用一张小表把三条链的结果记下来:解析解特殊点值、时域数值最大误差、频域相对误差。三个数都在预期量级,这道题才算过;任何一个超出一个数量级,回去查参数。
3. 把验证脚手架写成脚本:三个校验器覆盖八成题型
3.1 时间轴与采样率:一个函数管住所有题
做完十道题之后你会发现,大部分时间花在重复写时间轴、重复定 dt 上。把这几行抽成函数,后面每道题只写解析表达式和参考解。
import numpy as np def time_axis(t_end, fastest_tau, points_per_tau=100): """按最快时间常数自动定步长,返回 (t, dt)""" dt = fastest_tau / points_per_tau n = int(np.ceil(t_end / dt)) return np.arange(n) * dt, dt def ct_conv(x, h, dt, t0_x=0.0, t0_h=0.0): """连续卷积的离散近似,起点按时移对齐返回 (t, y)""" y = np.convolve(x, h) * dt t = (t0_x + t0_h) + np.arange(len(y)) * dt return t, ytime_axis 的第二个参数 fastest_tau 是这道题里衰减最快的那个时间常数,points_per_tau 取 100 是保守值,遇到振荡信号(比如 e^{-t}sin(10t))要按振荡周期而不是衰减常数来定,改用每周期 20 到 50 点。
ct_conv 里 t0_x 和 t0_h 这两个时移参数是血泪经验换来的。信号带时移(比如 u(t-2))时,卷积结果的起点是 t0_x + t0_h,而不是 0。如果忘了这一步,时间轴整体偏移,和解析解一比就到处对不上,而你大概率会先去怀疑步长不够,白白调半天。
3.2 三个校验器:卷积、变换对、差分方程
除了卷积,课后题主要就三类:拉普拉斯/Z 变换对、系统响应(冲激响应、阶跃响应)、差分方程递推。每类写一个校验器,用同一个报错格式,跑完一批题直接看哪几道红了。
变换对的校验用部分分式展开,scipy 的 residue 可以直接把有理函数拆成留数和极点:
import numpy as np from scipy.signal import residue # H(s) = (s + 3) / (s^2 + 3s + 2) = 2/(s+1) - 1/(s+2) b = [1, 3] a = [1, 3, 2] r, p, k = residue(b, a) for ri, pi in zip(r, p): print(f"留数 {ri:+.4f} 极点 {pi:+.4f}") print("直接项 k =", k)输出是留数 [2, -1]、极点 [-1, -2],对应 h(t) = (2e^{-t} - e^{-2t})u(t),和手算一致。这里的参数顺序要注意,residue 的 b 是分子系数(按 s 的降幂),a 是分母系数,两边都要补齐缺项,比如 s^2 + 2 要写成 [1, 0, 2],少写一个 0 结果全错。
差分方程的校验换个思路:给系统喂一个正弦,等稳态之后测幅度,和频率响应 |H(e^{jΩ})| 比。
import numpy as np from scipy.signal import lfilter a = 0.9 b_coef = [1.0] a_coef = [1.0, -a] # y[n] = x[n] + 0.9 y[n-1] w0 = 0.4 * np.pi n = np.arange(4000) y = lfilter(b_coef, a_coef, np.sin(w0 * n)) tail = y[3000:] # 跳过暂态 amp_meas = np.sqrt(2 * np.mean(tail ** 2)) # 正弦 RMS 反推幅度 amp_ref = 1 / np.abs(1 - a * np.exp(-1j * w0)) print("measured:", amp_meas, "theory:", amp_ref)用 RMS 反推幅度而不是取最大值,是因为采样点未必正好落在峰值上,w0 = 0.4π 时每周期只有 5 个点,取最大值会低估接近 20%。跳过前 3000 点是因为一阶系统的暂态衰减到 1e-3 需要大约 65 个时间常数,3000 点已经绰绰有余。这个校验器的价值在于它验证的是时域实现和频域理论的一致性,比单纯比对公式更有工程味。
3.3 残差报告怎么写才有用
跑完一批题,输出不能只有一句「通过」。我一般让每个校验器返回三个字段:题号、最大绝对误差、最大相对误差,最后拼成一张表。绝对误差看的是量级对不对,相对误差看的是形状对不对,两者要一起看。
举个例子,卷积题如果绝对误差 1e-4、相对误差 1e-3,说明参数都对;如果绝对误差很小但相对误差 0.5,大概率是解析解在某个区间接近零,分母把误差放大了,这时候要检查是不是区间截取错了;如果两个都大,先看时间轴起点,再看 dt,最后才怀疑解析解。
这张表攒到二十道题以后会非常有用:你能看出自己哪一类题的错误率高。我当时发现自己所有带时移的题都偏,回头一查全是 ct_conv 里没传 t0,这就是把踩过的坑变成了检查项。
4. 连续题和离散题混着做:四个必须先对齐的换算
4.1 拉普拉斯与 Z 变换:收敛域不是装饰
课后答案最常见的偷懒是只给表达式不给收敛域。H(s) = 1/(s+1) 到底是因果系统还是反因果系统,光看表达式分不出来,必须看 ROC。这一点在做逆变换和判断稳定性时是决定性的:同一个表达式,ROC 在 Re(s) > -1 对应右边信号 e^{-t}u(t),ROC 在 Re(s) < -1 对应左边信号 -e^{-t}u(-t),两者完全不同的时域波形。
离散侧同理。H(z) = 1/(1 - 0.9z^{-1}),若 ROC 是 |z| > 0.9,对应 0.9^n u[n],稳定;若 ROC 是 |z| < 0.9,对应 -0.9^n u[-n-1],不稳定。数值验证的时候,如果你的信号生成方式和 ROC 不匹配,跑出来的结果和理论永远差一个符号,而且这个符号很难从曲线上看出来。
我在脚手架里加了一个约定:每道题先写一行注释记 ROC,代码里生成信号时按 ROC 选 u[n] 还是 u[-n-1],这样后面出问题能第一时间排除这一项。
| 变换 | 定义式要点 | 收敛域 | 常见写错的地方 |
|---|---|---|---|
| 连续傅里叶 | ∫x(t)e^{-jωt}dt | 绝对可积 | ω 与 2πf 混用 |
| 拉普拉斯 | ∫x(t)e^{-st}dt,s = σ + jω | Re(s) > 极点实部最大值 | 单边变换误用于双边信号 |
| DTFT | Σx[n]e^{-jΩn} | 单位圆上收敛 | Ω 与 ω 混用,差一个 Ts |
| Z 变换 | Σx[n]z^{-n},z = re^{jΩ} | z |
4.2 频率归一化:ω、f、Ω 之间只差一个 2π 和一个 Ts
连续域的角频率 ω(rad/s)、普通频率 f(Hz)、离散域的归一化角频率 Ω(rad)三者之间的换算是错得最多的地方,而且错了之后现象很迷惑:波形看起来对,但周期差了一截。
关系是 ω = 2πf,Ω = ωTs = 2πf/fs。代码里 np.fft.fftfreq 返回的是 f(单位 Hz),要转成 ω 得乘 2π;而 scipy.signal.freqz 返回的是归一化频率(0 到 π),要转成 ω 得除以 Ts 再乘 2π……等一下,freqz 返回的 w 本身就是 Ω(rad/sample),不需要再除 Ts,它的物理含义是数字角频率,和连续域的 ω 差了采样率这一层。
我踩过的最典型的一次:用 freqz 算出一阶系统的截止频率,把返回值当成 rad/s 直接和模拟滤波器的指标对比,结论是「数字滤波器带宽只有设计值的一半」,排查了半天才发现是漏了乘以采样率。从那以后,我在脚手架里强制变量命名带单位后缀,w_rad_s、w_rad_sample、f_hz,混用会直接报错。
4.3 部分分式展开的数值稳定性与留数符号
residue 在极点接近时会出问题。教材题目一般都设计成互异极点,但如果你自己扩展到高阶系统,重极点会让 residue 返回一堆相邻极点和巨大留数,虽然数学上仍然正确(数值上极点轻微分裂),但生成的时域表达式会很长,看起来完全不像课本答案。
遇到重极点,正确做法是手工用 (s-p)^k 的形式推,或者改用符号计算(下一章会说)。数值方法在这里只能做验证,不能替代推导。
留数的符号也是高频错误点。residue 返回的 r 是 H(s) = Σ r_i/(s - p_i) + k(s),注意分母是 (s - p_i),当 p_i = -1 时就是 (s + 1),这个负号变化很容易在抄进代码时丢掉。我的做法是先打印出来,再和手算的留数逐个比对,对不上就停,不要往下跑。
5. 避坑与排查:五个把我按回桌前重算的细节
5.1 卷积的起点与时移对齐
现象:带时移的题,数值解和解析解在 t 轴上整体错开一段,误差曲线在边界处冒出一个大尖峰,中间段反而对得很准。
原因:np.convolve 只做下标卷积,不知道信号在物理时间上从哪开始。x(t) = u(t-2) 和 h(t) = e^{-t}u(t) 卷出来,结果的起点是 2,但代码默认从 0 开始铺时间轴。
解决:给 ct_conv 传 t0_x 和 t0_h,返回的 t 用两者之和做起点。养成习惯,只要信号里有 u(t - a),就把 a 记下来传进去。另外参考信号生成时也要用同一个 t 数组,别用 np.arange(len(y)) * dt 重新造一个。
5.2 单边与双边变换混用
现象:逆拉普拉斯变换的结果符号和答案相反,或者多出一个 u(-t)。
原因:答案用的是双边变换或者带 ROC 的逆变换,代码里按因果信号处理了。sympy 的 inverse_laplace_transform 默认按因果解释,scipy 的 residue 只给代数分解,两者都不管 ROC。
解决:先在注释里写清信号是右边、左边还是双边,再选对应的生成方式。验证时把 t 轴扩展到负半轴,左边信号在 t<0 有值,右边信号在 t<0 为 0,一眼就能看出来。
5.3 采样率不足导致的假对齐
现象:低通滤波器的频率响应用 DFT 验证,低频段完全吻合,高频段误差很大,但你怎么调 N 都压不下去。
原因:dt 太大,奈奎斯特频率低于你比较的最高频率,高频段的 DFT 结果已经混叠了。
解决:比较窗口的上限不超过 0.4/dt(留 20% 余量),或者干脆把 dt 减半再看误差是否随 dt 线性下降。误差随 dt 一阶下降说明是矩形积分误差,不下降说明是混叠或者截断,这两个原因用这个办法能分开。
5.4 序号从 0 还是从 1 开始
现象:离散题的结果整体移位一位,比如 h[n] = a^n u[n] 算出来 h[0] 对不上。
原因:有些资料写 u[n] 从 n=1 开始,有些从 n=0 开始;MATLAB 系的索引从 1 开始,Python 从 0 开始。
解决:全篇统一用「n ≥ 0 时 x[n] 有值」的约定,代码里 np.arange(N) 就是 n。遇到和答案差一位的情况,先检查这个约定,不要急着改解析解。
5.5 分母补零与系数顺序
现象:residue 报错「First denominator polynomial must be nonzero」,或者返回的极点完全不对。
原因:多项式系数没有按降幂补齐,比如 s^2 + 2 写成了 [1, 2],被解析成 s + 2。
解决:写一个小的构造辅助函数,输入极点列表直接生成系数,避免手写数组:
import numpy as np def denom_from_poles(poles): """由极点列表生成降幂排列的分母系数""" return np.real(np.poly(poles)).tolist() print(denom_from_poles([-1, -2])) # [1.0, 3.0, 2.0] 即 s^2 + 3s + 2这类小工具攒到十几个之后,写题的速度会明显变快,出错率也降下来了。每一类题先手算一个标准答案存成测试用例,后面遇到同类题直接跑,这个习惯比背答案有用得多。
6. 用符号计算给每道题配一个自动判卷器
数值验证有个天花板:它只能告诉你「这条路径算出来的数对不对」,不能告诉你「表达式能不能化简成课本那个形式」。这时候上符号计算,把推导也交给机器。
import sympy as sp s, t, tau = sp.symbols('s t tau', positive=True) H = (s + 3) / (s ** 2 + 3 * s + 2) h = sp.inverse_laplace_transform(H, s, t) print("h(t) =", sp.simplify(h)) # 2*exp(-t) - exp(-2t) # 直接做卷积积分,验证 2.1 节的解析解 x = sp.exp(-tau) y = sp.integrate(x * sp.exp(-2 * (t - tau)), (tau, 0, t)) print("y(t) =", sp.simplify(y)) # exp(-t) - exp(-2t)两段输出和手算完全一致,而且符号计算不引入步长误差,这是它比数值方法强的地方。参数上唯一的讲究是 symbols 的 positive=True,不加的话 sympy 会给你一堆分段函数,看起来吓人,其实只是它在考虑 t<0 的情况。
我的自动判卷流程分三层:符号层判表达式是否等价(用 sp.simplify(a - b) == 0),数值层判特殊点是否吻合,图形层人眼看一眼形状。三层里符号层最快,数值层最可靠,图形层只做兜底。三者组合,八成课后题都能在几分钟内确认。
顺手记一张对照表,下次遇到同类题直接查:
| 题型 | 首选校验手段 | 精度预期 | 主要风险 |
|---|---|---|---|
| 连续卷积 | 时域数值 + 频域相乘 | 绝对误差 1e-4 量级 | 时移起点没对齐 |
| 逆拉普拉斯 | sympy 逆变换 + 特殊点 | 表达式级精确 | ROC 没约定导致分段 |
| 差分方程 | lfilter 稳态幅度 | 相对误差 1e-3 量级 | 暂态没跳干净 |
| 频率响应 | DFT 低频段比较 | 相对误差 1e-3 量级 | 比较窗口超出奈奎斯特 |
说个我自己的教训。刚开始做这套东西的时候,我把精力全放在调误差上,误差大就加采样点,加到内存爆掉还是不对,后来才发现是时间轴起点错了,加多少点都没用。所以现在的顺序是:先对齐物理量(起点、单位、ROC),再看数值精度,最后才动参数。数值方法是个黑匣子,你喂进去的物理约定错了,它只会安静地给你一个看起来很漂亮的错误答案。
这套流程做顺之后,一份课后答案 PDF 的价值就不在「对答案」上了,而在「给你一套标准输出」,剩下的推导和验证都是你自己长出来的能力。希望帮到你。
本文还有配套的精品资源,点击获取