简介:这份《信号与系统》课后答案PDF面向电子工程、通信工程等专业学生及备考南京邮电大学信号与系统分析的学习者,聚焦课程习题的对照与复盘,尤其适合在周期性判断、离散序列周期、信号合成与抽样等基础章节查漏补缺。资源共1个PDF文件,约1.12MB,内容围绕第一章基本概念展开,涵盖连续时间信号与离散时间序列的周期性判定、基本周期求解、周期信号相加条件证明、均匀抽样后序列波形分析,以及能量信号、功率信号与非能非功信号的判别,并给出较完整的推导过程和结论。已有3935人学习,说明其在课程同步练习和期末复习中有一定参考价值;读者可据此核对解题思路,理解频率比是否为有理数、抽样间隔与数字角频率之间的关系,并借助典型例题梳理易混概念。整体篇幅集中、查阅方便,适合作为课堂笔记之外的辅助答案材料。
1. 信号与系统课后答案 PDF 不是答案书:先当校验器用
某同学把信号与系统课后答案 PDF 打印成册,一章一章对,红笔勾完觉得稳了,期中卷子发下来 52 分。翻他的本子,三百多道题只有一行行“与答案一致”,没有一行写“为什么这一步能这么变”。
问题不在答案,在于把答案当成了终点。信号与系统这门课的答案有个特殊之处:同一个结果能有五六种写法——指数形式、三角形式、留数形式、部分分式形式,看起来不一样的和真算错的,外观上几乎无法区分;反过来,看起来一样的,也可能藏着收敛域漏写、2π 位置错、单边双边混用这类致命问题。
所以这份 PDF 的正确用法是当校验器:先自己算,再拿它投票;对不上时按约定的差异逐条排查,而不是照着抄一遍步骤。这篇写给正在啃这门课、手里已经有一份课后答案 PDF 的人,也写给想把对答案这件事沉淀成可复用流程的人。
2. 从 PDF 到可检索题库:文本层判定与题号重建
拿到一份几百页的答案 PDF,第一反应不该是打开第一页开始读,而是先弄清楚它的排版属性。文本层、扫描层、混合层,三种情况对应的处理成本差十倍以上:文本层半小时能建完索引,扫描层要先渲染再识别、公式区域还得单独裁,混合层最麻烦,正文能抽但公式变成了图片,抽出来的文本会在关键位置断掉。先花五分钟做判定,能省掉后面几天的返工。
2.1 三行统计判断这份 PDF 是文本层还是扫描层
pdfplumber 把每一页解析成字符对象、图片对象和线条对象的集合。文本层页面的chars数量通常上千,扫描层几乎是 0,因为整页就是一张位图。
import pdfplumber PDF_PATH = "signals_systems_answers.pdf" with pdfplumber.open(PDF_PATH) as pdf: print("总页数:", len(pdf.pages)) for i, page in enumerate(pdf.pages[:20], start=1): n_chars = len(page.chars) # 字符对象数,文本层页面通常上千 n_images = len(page.images) # 图片对象数,扫描页通常 1~2 text = page.extract_text() or "" n_lines = len(text.splitlines()) print(f"p{i:03d} chars={n_chars:5d} images={n_images:2d} lines={n_lines:3d}")这段代码只做一件事:抽样前 20 页,把三个指标打出来。逻辑说明上没什么玄学,关键是阈值怎么定。chars的绝对数量受字号、页边距、公式密度影响很大,所以别死记一个数,先看分布:如果 20 页里有 18 页的chars都在 800 以上,剩下 2 页低,那多半是插图页而不是扫描页;如果整批chars都低于 100 而images稳定为 1,直接判定扫描件,走 OCR 路线。
参数上还有两个细节值得注意。extract_text()默认按字符的 top 坐标排序,遇到双栏排版会把左右两栏串成一行,视觉上很乱,但用于判断“有没有文本层”完全够用。真正抽取阶段要换成extract_text(layout=True),它保留列间空白,双栏串行会明显减少。
| 类型 | 前 20 页表现 | 处理方式 | 预计耗时 |
|---|---|---|---|
| 纯文本层 | chars 普遍 > 800,lines 连续 | pdfplumber 直接抽,正则切题号 | 半小时到两小时 |
| 扫描层 | chars < 50,images ≥ 1 | 300dpi 渲染后 OCR,公式单独裁图 | 一到三天 |
| 混合层 | 部分页 chars 在 200~800 之间 | 正文走文本层,公式段落人工补或裁图识别 | 半天到两天 |
混合层的识别有个小技巧:按行统计字符密度。正文段落每行的chars分布稳定,公式行会骤降,找到密度断崖的位置,就找到了公式区。这一步不用写复杂代码,把每页的len(line)打印出来扫一眼就能看出来。
2.2 信号与系统课后答案 PDF 的题号抽取:从整页文本到三列索引
题号是整份答案 PDF 里最稳定的锚点。不管答案正文多乱,题号一定出现在每道题的开头,而且格式高度可预测:2-15、2.15、2.15(a)、题 2-15,基本逃不出这四种。
import re, csv, pdfplumber QID = re.compile( r"^\s*(?:题\s*)?(\d{1,2})\s*[-–.]\s*(\d{1,3})\s*" r"(?:[((]\s*([a-z0-9])\s*[))])?" ) def split_by_anchor(page_text, pattern): """把一页文本按题号锚点切成 (锚点行, 正文块) 列表""" lines = page_text.splitlines() idx = [i for i, ln in enumerate(lines) if pattern.match(ln)] blocks = [] for k, i in enumerate(idx): j = idx[k + 1] if k + 1 < len(idx) else len(lines) blocks.append((lines[i], "\n".join(lines[i + 1:j]).strip())) return blocks rows = [] with pdfplumber.open(PDF_PATH) as pdf: for pno, page in enumerate(pdf.pages, start=1): # 裁掉上下各 5% 的页眉页脚,避免页码混进正文块 body = page.crop((0, 0.05 * page.height, page.width, 0.95 * page.height)) text = body.extract_text(layout=True) or "" for head, content in split_by_anchor(text, QID): m = QID.match(head) chap, num, sub = m.group(1), m.group(2), m.group(3) or "" rows.append({ "page": pno, "qid": f"{chap}-{num}{sub}", "head": head.strip()[:100], "content": content[:2000], }) with open("answer_index.csv", "w", newline="", encoding="utf-8-sig") as f: w = csv.DictWriter(f, fieldnames=["page", "qid", "head", "content"]) w.writeheader() w.writerows(rows) print("抽到题号条数:", len(rows))逻辑说明里最重要的是“为什么先抽锚点再切块”,而不是一次性用正则把题号和答案一起抓出来。答案正文可能跨页、跨栏、跨段,一次性正则几乎必然在跨页处崩掉。先拿到“题号 → 起始行”的锚点序列,再按相邻锚点之间的区间切块,跨页的那道题会自动把下一页开头的内容吞进自己的块尾——这恰好是想要的行为。
参数说明上,crop的 5% 是个经验值,主要针对页脚页码和页眉书名;如果你的 PDF 页边距特别窄,这个比例要往下调,否则会切掉正文首行。content截到 2000 字符是为了控制 CSV 体积,真要完整正文,把这个上限放开、或者改成把每块单独存成一个 txt 文件,索引 CSV 里只留页码和锚点行。中文编码固定用utf-8-sig,否则用表格软件打开会乱码。
layout=True在双栏页面上仍有局限:题号可能被并到行中间,这时把pattern.match换成pattern.search,同时在切块前把长行按两个以上连续空格劈开。这个修补不复杂,但一定要在抽完之后抽查,别直接进下一步。
2.3 题号体系对不上时,用一张映射表把两本书钉在一起
真实情况里,答案 PDF 的题号和课本的题号经常不是一套。有的按节编号,有的按章编号,有的答案里连小节标题都没了只剩纯序号。硬对齐会浪费大量时间,一张三列映射表就能解决。
| book_qid | answer_qid | match_type | note |
|---|---|---|---|
| 2-15 | 2-15 | exact | 直接命中 |
| 2-16 | 2-17 | offset | 答案 PDF 多插了一道例题 |
| 2-18 | missing | 答案 PDF 无此题,去别处找 | |
| 2-19a | 2-19 | manual | 答案未分子问,需人工拆 |
生成这张表的思路是先把两边的题号都列成有序集合,求差集,看差集是不是常数偏移。是的话批量加偏移量;不是的话,把连续匹配的段落找出来,只在断点处人工干预。一份三百题规模的答案 PDF,自动对齐能覆盖 70% 到 85%,剩下的二三十题手工标一遍,二十分钟的事。
提示:
missing那几行别急着补。有些课后题本来就是证明题或编程题,答案 PDF 里没有是正常的,先把它们单独拉一个列表,避免每次翻到都重新确认一遍。
3. 逐题反向校验:用 sympy 把变换、卷积和系统响应重算一遍
对答案这件事最容易翻车的地方,是只比对最后一行。信号与系统的题,最后一行往往是某种化简形式,两条完全不同的推导路径可能收敛到同一个式子,也可能收敛到两个看起来不一样但等价的式子。真正可靠的比对方式是走三条互相独立的路径,三条都过才算通过。
3.1 三个校验锚点:可逆性、极点与收敛域、变换域乘积
第一个锚点是可逆性。正变换做一遍,再反变换做一遍,看能不能回到原函数。这个检查抓的是定义层面的一致性——如果你的正变换用的是教材约定,反变换也必须用同一套,中间混入别的约定会立刻暴露。
第二个锚点是极点位置与收敛域。极点决定自由响应的形式,收敛域决定因果性与单双边。很多答案 PDF 只写极点不写收敛域,或者只在章首统一声明“以下默认因果”,这种省略在简单题里没事,在多值反变换的题里就是灾难。
第三个锚点是变换域乘积等于时域卷积。这条路径和前两条完全独立:前两条都在同一个变换域里打转,第三条跨越两个域,一旦结果一致,基本可以排除计算错误。三条都过还和答案对不上,那八成是约定差异,不是算错。
3.2 拉普拉斯与 Z 变换的校验脚本骨架
import sympy as sp t, s, z, n = sp.symbols('t s z n', real=True) w = sp.symbols('omega', real=True) f_repr = sp.symbols('f_repr', real=True) # sympy 傅里叶变换的默认自变量 def ctft(expr): """封装成教材约定 F(jw) = ∫ f(t) e^{-jwt} dt""" F_f = sp.fourier_transform(expr, t, f_repr) # sympy 内部用 e^{-j2πf t} return sp.simplify(F_f.subs(f_repr, w / (2 * sp.pi))) def ztrans(expr): """单边 Z 变换,sympy 无内置函数,用求和代替""" return sp.simplify(sp.summation(expr * z**(-n), (n, 0, sp.oo))) # 例:f(t) = e^{-3t} u(t) f = sp.exp(-3 * t) * sp.Heaviside(t) Fs = sp.laplace_transform(f, t, s, noconds=True) print("L{f} =", sp.simplify(Fs)) # 1/(s + 3) print("回验 =", sp.simplify(sp.inverse_laplace_transform(Fs, s, t))) print("CTFT =", ctft(f)) # 1/(3 + I*omega) # 例:x[n] = a^n u[n] a = sp.symbols('a', real=True) print("Z{x} =", ztrans(a**n * sp.Heaviside(n)))逻辑说明部分,重点看两个封装。ctft这个 wrapper 不是可选的,是必须的:sympy 的fourier_transform用的是e^{-j2πft}定义,和绝大多数教材的e^{-jωt}差一个 2π 换元。直接拿输出和答案比对,会得到“差 2π”的假警报,然后开始怀疑自己或者怀疑答案,浪费一晚上。这里做的替换是f = ω/(2π),注意只有一个替换、没有额外系数,因为两个定义都是关于各自自变量的密度函数。
ztrans用求和实现,原因是 sympy 没有内置 Z 变换。这让它天然只支持单边(n 从 0 到无穷),双边 Z 变换要把下界改成-sp.oo。另外sp.summation返回的结果经常带Piecewise条件,那个条件就是收敛域的一部分,别急着用simplify把它抹掉,先看一眼条件里的不等式是不是|a/z| < 1这种形式。
参数说明还有一处容易忽略:Heaviside(0)在 sympy 里默认是1/2,教材里多数写u(0)=1。这个差异在纯符号推导里看不出来,但在画图、做初值定理、或者跟数值结果比对时会显形。要么全程用sp.Heaviside(t, 1)显式指定,要么在结论里注明这个约定差异。
3.3 卷积和差分方程:两条互相独立的验证路径
离散卷积用 numpy 最快,但索引起点必须自己记,这是最容易被忽略的一步。
import numpy as np x = np.array([1, 2, 3]) # x[0]=1, x[1]=2, x[2]=3 h = np.array([1, -1]) # h[0]=1, h[1]=-1 y = np.convolve(x, h, mode='full') n0 = 0 + 0 # 起点 = x 起点 + h 起点 print(n0, y) # 0, [1 1 1 -3] # 连续卷积:y(t) = ∫ x(τ)h(t-τ)dτ tau = sp.symbols('tau', real=True) xt = sp.exp(-2 * t) * sp.Heaviside(t) ht = sp.Heaviside(t) yt = sp.integrate(xt.subs(t, tau) * ht.subs(t, t - tau), (tau, -sp.oo, sp.oo)) print(sp.simplify(yt)) # (1 - exp(-2t)) * Heaviside(t) / 2离散那段的逻辑说明:np.convolve只关心数值,不关心时间轴。如果x是从n=-2开始的序列,你传进去的是[x[-2], x[-1], x[0], ...],输出的第一个元素对应的是n=-2+0=-2,不是 0。所以每次卷积前先把两个序列的起点写下来,卷积后把n0补上,否则答非所问。
连续那段两个subs的写法值得注意:积分变量必须是tau,而不是t。x(t-τ)要写成xt.subs(t, t - tau),h(τ)写成ht.subs(t, tau)——顺序很容易搞反,搞反之后得到的是相关而不是卷积,结果对称性完全不同,但在某些例子里数值恰好一样,属于典型的“错得看不出来”。
差分方程这条路径用scipy.signal独立验证:
from scipy import signal b, a = [1.0], [1.0, -0.8] # y[n] - 0.8y[n-1] = x[n] _, h = signal.dimpulse((b, a, 1), n=8) print(h[0].ravel()) # [1, 0.8, 0.64, ...]这条路径的价值在于它绕开了符号推导:dimpulse直接由系数递推,和手推的h[n]=0.8^n u[n]对不上,说明系数抄错了或者方程整理错了。三步验证下来,还和答案 PDF 不一致,那基本可以确定是约定问题而非计算问题。
4. 符号约定对照:答案和你课本对不上的六组地方
约定差异是这类答案 PDF 最集中的坑源。同一道题,你的推导和答案逐行不同,但可能都对,只是站在不同的定义上。把下面六组先对齐,能少掉大半的自我怀疑。
4.1 变换定义里的 2π 与正负号
| 项目 | 常见约定 A | 常见约定 B | 换算关系 |
|---|---|---|---|
| 连续傅里叶变换 | F(ω)=∫f(t)e^{-jωt}dt | F(f)=∫f(t)e^{-j2πft}dt | F(ω)=F(f)取f=ω/2π |
| 反变换系数 | 1/(2π)在反变换前 | 1在正变换前 | 系数整体平移 |
| 指数符号 | 正变换e^{-jωt} | 正变换e^{+jωt} | 频谱整体共轭翻转 |
| 傅里叶级数 | 指数型系数含1/T | 三角型系数含2/T | a_n、b_n与c_n差因子 2 |
| Z 变换 | 用z^{-n} | 用z^{n} | 收敛域内外互换 |
| DFT 与 DTFT | X(e^{jω})连续 | X[k]离散 | ω=2πk/N |
这张表最实用的地方是第二行和最后一行。系数位置决定了你在做能量计算、帕塞瓦尔定理验证时会不会差一个2π;而 DFT 与 DTFT 的关系决定了你在用 FFT 做数值验证时,横轴该用k还是ω,以及幅值要不要除以N。
注意:
numpy.fft用普通频率形式且不做1/N归一化,教材用角频率形式且在反变换处带1/(2π)。用 FFT 验证手算频谱时,横轴换算、幅值换算、相位符号三样都要动,缺一不可。
4.2 收敛域、冲激尺度与初值终值定理的适用边界
收敛域这条经常被简化成“默认因果”。简化在单边题里没问题,但在双边题里直接错。以F(s)=1/(s+a)为例,ROC: Re(s) > -a对应e^{-at}u(t),ROC: Re(s) < -a对应-e^{-at}u(-t),两个结果符号相反、支撑区间相反,只写极点位置是分不出来的。离散同理,H(z)=1/(1-az^{-1}),|z|>|a|是右边序列,|z|<|a|是左边序列且带负号。
冲激尺度这条考的是δ(at)=δ(t)/|a|。很多答案 PDF 在化简过程中直接用了这个等式却没标出来,你按原式推就会差一个系数。相关的还有u(t)在 0 点的取值,以及δ(t)与u(t)的求导关系在 0 点附近的处理——这些细节在纯符号层面无害,但在数值仿真里会变成一两个采样点的偏差,采样率越高偏差越明显。
初值定理和终值定理各有前提,条件不满足时公式给出的是错误答案而不是近似答案。终值定理要求sF(s)的极点全部位于左半平面(离散情形是(z-1)X(z)的极点全部位于单位圆内),只要虚轴上有一对共轭极点,稳态值就不存在,套公式算出来的数字没有任何意义。初值定理要求F(s)是真分式,分子阶数高于或等于分母时先做长除。
4.3 部分分式展开:系数对但形式错的三种典型
第一种是假分式没做长除。分子阶数不低于分母时,直接展开会得到一堆看似合理的系数,但缺少多项式部分,反变换里少了冲激项。
第二种是重极点。F(s)=1/(s+a)^2需要写成A/(s+a) + B/(s+a)^2两项,只写一项的话系数怎么算都不对,或者算出来只有一个系数被强行凑上。三重极点要三行,规律是先按最高次写到一次。
第三种是共轭极点写成了复指数还是三角。1/(s^2+2s+5)的极点是-1±2j,写成e^{-t}(Acos2t + Bsin2t)和写成两个复指数项再合并,最终结果一致,但中间系数完全不同。拿答案比对时,先在同一个形式下对齐,再去比数值。
留数法求系数有个隐藏前提:只对单极点成立。(s+a)F(s)在s=-a处取值,这个操作在单极点时给出A,在重极点时给出的只是最高次项的系数,剩下的还得再求导。翻车现场通常是系数算出来一半对一半错,然后怀疑是符号问题。
5. 避坑与排查:拿课后答案刷题最常见的 5 类翻车
下面五条都是“看起来算对了、实际错了”的类型,共同特点是结果和答案长得像,只是差一点点。
5.1 五条血泪记录:现象、原因、解决
第一条:变换结果一致,反变换差一个u(t)或差一个负号。
现象是拉普拉斯变换算出来和答案完全一样,反变换回去少了阶跃、或者符号反了,怎么检查都找不到算术错误。原因是收敛域没写,题目默认的是单边还是双边没有确认,答案 PDF 在章首统一声明过一次“本章默认因果”,你翻到中间早就忘了。解决办法是每题强制在变换结果后面手写一行ROC:,双边题尤其要写清楚。写不出来就说明这题还没做完。
第二条:离散卷积结果比手算短一截,或者多出尾部一项。
现象是np.convolve的输出长度是len(x)+len(h)-1,和手算对不上,或者数值对得上但时间轴整体平移了。原因是序列起点不是 0,np.convolve不知道这件事。解决办法是全程维护起点变量n0 = nx0 + nh0,输出索引显式写成n0 + np.arange(len(y))。养成习惯之后这类错基本绝迹。
第三条:sympy 的傅里叶变换结果多了或少了 2π。
现象是用sp.fourier_transform算出来的式子和答案差一个常数因子,或者ω位置上出现2π。原因是 sympy 用e^{-j2πft}定义。解决办法是用本文 3.2 的 wrapper 做一次f=ω/(2π)替换,不要手工调系数,手工调必然在一次次的题里调错方向。
第四条:终值定理算出稳态值,数值仿真却发散。
现象是套lim_{s→0}sF(s)得到一个有限数,但用scipy.signal跑阶跃响应,输出一路涨上去。原因是系统在虚轴上有极点,稳态根本不存在。解决办法是在套公式之前先算极点:np.roots(a),实部只要有一个 ≥ 0 就停下来,别往下算。这条在考试里也是高频陷阱。
第五条:采样定理相关的题,答案差整整 2 倍。
现象是奈奎斯特频率算出来是答案的一半或两倍。原因是角频率和普通频率混用了,rad/s与Hz之间差一个2π,而很多答案 PDF 在题干里用rad/s、在结论里写Hz,中间不声明。解决办法是全程带单位:先把所有频率统一到Hz,算完再换回去,中间过程一个2π都不许省。
5.2 一份五分钟排错清单
对不上答案时,按顺序过一遍,绝大多数问题在前三步就能定位。
| 顺序 | 检查项 | 判断依据 | 处理 |
|---|---|---|---|
| 1 | 单位是否统一 | 频率是 Hz 还是 rad/s | 全部换成 Hz 再比 |
| 2 | 收敛域是否写出 | 双边题有没有 ROC 行 | 补 ROC 后重做反变换 |
| 3 | 序列/信号起点 | 离散题起点是不是 0 | 补 n0 偏移 |
| 4 | 单边还是双边 | 变换是否含 t<0 部分 | 与章首声明对齐 |
| 5 | 变换定义约定 | 2π 与正负号 | 用 wrapper 重算 |
| 6 | 形式是否等价 | 三角 vs 指数 | 统一形式后再比数值 |
第六步是最容易被跳过的一步。两个形式上完全不同的表达式可能恒等,尤其在含sin、cos、复指数的结果里。判断方法很土但有效:随便代三个数值进去,看数值是否一致。三个点都对上,基本可以认为等价。
6. 数值反演兜底:当答案、课本和你的推导三方打架时
符号推导走到最后三方不一致,继续在纸上纠缠的收益很低,这时候让数值投票。
思路是把系统函数直接喂给scipy.signal,算出频响和冲激响应,再和手算结果在几个频点、几个时间点上比数值。这一步不关心形式,只关心数。
import numpy as np from scipy import signal # H(s) = 1/(s^2 + 3s + 2),手算应为 1/((s+1)(s+2)) sys_c = signal.lti([1.0], [1.0, 3.0, 2.0]) w = np.array([0.1, 1.0, 10.0]) _, H = signal.freqresp(sys_c, w=w) print(np.round(np.abs(H), 6)) print(np.round(np.angle(H), 6))比对时看两个量:幅值和相位。手算的|H(jω)| = 1/sqrt((2-ω²)² + 9ω²),在ω=1处是1/sqrt(1+9)=0.3162,脚本输出对上就说明分母系数没抄错。相位更容易暴露符号问题,ω=0.1时相位接近 0,ω=10时接近-π,如果符号反了,一眼就能看出来。
连续系统想验证离散结论,用cont2discrete做一次离散化再跑lfilter,把离散冲激响应和手算的h[n]逐点比。这一步的价值在于它同时验证了 Z 变换结果、差分方程系数、以及收敛域对应的因果性——三条线上任何一条错了,冲激响应的形状都会变。
我自己踩得最深的一次,是一道二阶系统的阶跃响应,答案 PDF 写的是欠阻尼,我算出来是临界阻尼,反复检查了四遍推导都没找到问题。最后把阻尼比代进signal.lti跑了一遍,发现是我把特征方程的中间项系数抄成了2,实际是2√2,一个根号被我漏掉了。纸面推导里根号和数字长得很像,眼睛会自己骗自己。
从那以后我养成一个习惯:凡是含根号、含2π、含分数的题,算完一定用数值过一遍,哪怕只代一个点。符号推导负责给形式,数值负责给信心,两样都有了,才敢在答案 PDF 上打那个勾。希望帮到你。
本文还有配套的精品资源,点击获取