傅里叶变换
1. 连续傅立叶变换(Continuous Fourier Transform)
对于时域连续函数 ,它的傅立叶正变换(FT)定义为
(用角频率
表示) 或者
(用频率
表示,
)
傅立叶逆变换(inverse FT)定义为
2. 离散傅立叶变换(Discrete Fourier Transform, DFT)
如果有等时间间距 的时域信号N个:
,它的离散傅立叶变换(DFT)定义为
这里 . 相应的逆变换定义为
3. 快速傅立叶变换(Fast Fourier Transform, FFT)
快速傅立叶变换FFT是一种更加高效的计算离散傅立叶变换的算法,有着 的计算复杂度,比原始的DFT
计算复杂度有更好的可扩展性。Cooley-Tuckey算法是一种常见的FFT实现方式,但是需要限制输入数据点数
(2的乘方)形式。
如果你的数据数量不是2的几次幂,可以通过在后面加0(zero padding)的方式凑成下一个2的乘方个数据,添加的0不会影响频域谱,只会增加表观数据密度,即多出来的频域点其实是有效数据的线性内插值 Linear interpolation,如下图所示:
对于zero padding证明如下:
如果记原始时域信号的离散傅立叶变换为 ,其中时域数据数为
. 我们添加0到后面,记zero padding的信号为
, N 是2的乘方。所以
的离散傅立叶变换为
离散傅立叶变换得到的频域信号是以 为间距的,最大频率称做Nyquist频率,即为
,它只跟时域信号的步长有关。经过FFT变换的频域信号对应的下列频率
也就是说,前一半是正的频率,后一半是负的频率,中间点是正负最大频率。一般画图的话,需要把后一半挪到前面。
连续傅里叶变换
如果写成一般形式,用 替换原信号,用
替换40,得到
问题来了,虽然貌似联系很紧密,但这怎么跟DFT的公式长得不一样。。。DFT的公式应该是这样的:
就算用欧拉公式展开,我们得到的是
链接:https://www.zhihu.com/question/21314374/answer/542909849
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
觉得现有答案都是从连续傅里叶变换引出离散的,各位大神功底很扎实啊。但问题是很多同学就是因为对那些繁琐的 和
感到晕厥,才转到没有积分,只有
,看着比较简单的DFT来试试。所以DFT这样明显更简单的一个东西为什么一定要从连续傅里叶变换出发来讲呢?换个思路不行吗?
本文直接从DFT出发,不需要连续傅里叶变换的知识。
DFT做了什么事?
对一段有限长的离散信号,找出它含有的各个频率的正弦波分量。(这是句大家都知道的废话)
但如果换个单独针对DFT的解释就会好理解很多:
在长度为N的离散信号中,针对k=(0,1,2...),分别找出在长度N内振动k个周期的三角波分量的权值
怎么做呢?举个例子,大家可以打开MATLAB一起做
抛开时间抛开采样频率,只看点的个数,我们对某个余弦信号在两个周期内采样了40次:
n = 0:39;
y = cos(2*pi*(2*n)/40);
stem(n,y);

现在我们想通过DFT知道它在40次采样时间内振动了几个周期,算法是怎么处理的呢?其实很暴力:
事先选取40个长度为40个点的基信号,它们分别长这样:
第一个,40次采样内振动0个周期: ,即常值

第二个,40次采样内振动1个周期:

第三个,40次采样内振动2个周期:

第四个,40次采样内振动3个周期:

第五个,40次采样内振动4个周期:

……
以此类推一直到40个采样内振动39个周期。
接下来,对于上述40个基信号,判断它们跟原信号的相关程度。代数里判断两个长度相同的离散函数x[k],y[k]的相关程度,是用把它们在同一点的函数值相乘,再对得到的积求和的方法(就是向量求内积的方法),即下面的公式:
这个值越大,x[k]和y[k]长得越像。于是DFT把
到
这40个基函数都跟当前函数
比较了一下,发现
和
跟它长得最像!
这很显然,因为
于是
下面,如果我们把这40次每次比较的correlation值记下来,就得到了原信号在每个频率上的分量大小,比如,我要看它跟40次采样中振动8次的那个基信号相似程度,就是:

把这40个correlation值记下来,就得到了原信号的频域X:
stem(n, abs(fft(y)));

可以看到 ,其他值都是0
如果写成一般形式,用 替换原信号,用
替换40,得到
问题来了,虽然貌似联系很紧密,但这怎么跟DFT的公式长得不一样。。。DFT的公式应该是这样的:
就算用欧拉公式展开,我们得到的是
这又是为什么?
这是因为,对于一个信号,如果只跟余弦函数比较,会损失一些信息,比如相位。
如果我们的原信号有一些相位偏移,
如果对这个函数同样按照上面的方法计算频域,结果会有一些不一样:
n = 0:39;
x = cos(2 * pi * (2 * n)/40 + pi/4);
stem(n, real(fft(x)));

虽然对应频率还是筛选出来了,但是权值大小不一样了,如果计算一下得知
在相位偏移前,这个值是20,是原来的 倍
注意如果我们再找一个信号 ,没有相位偏移,但是直接把幅值砍到
,即:
那么这个信号的
跟 信号的结果一模一样,这样我们就无法通过频域恢复信号了,或者说我们损失了信息
解决方法是另选一组以正弦函数(实际上选了负正弦)为基准的“基信号”,即 到
,计算另一组原信号与正弦基的相关系数,这两组系数一起作为DFT的最终结果。而复数只是一个工具,用来方便地同时存储两组计算结果。当然它还有一个好处就是能够比较直观地表现出模和相位。
选负正弦还是正弦做基信号其实无所谓,只是最后的结果算出来的相位反一下而已,幅值是一样的。如果一个信号跟某频率余弦和负正弦的相关系数分别为a,b,那么代表这个信号差不多型如
根据高中数学可以求得其模为 ,(相对余弦的)相位为
,这与复数
的模和相位是相同的,因此DFT的公式
相当于同时把x[n]做了跟N个余弦基、N个负正弦基的比对,结果用N个复数存储。如果想要看某个频率的相位和模,就看对应复数的相位和模。
我们再来看看上面有相位偏移的那个例子:
原信号:
与余弦比对:
与负正弦比对:
在40个点内振动两个周期这个频率上,其DFT的结果为
其模为20,与相位偏移前相同。
其相位为 ,也没有问题。
如果将原信号变为 ,会求得该频率DFT结果为
,求得其相位为
。因此,根据DFT结果求得的相位是相对余弦信号的相位。
对于实际信号
上面做的分析没有考虑时间和采样频率,但只要给定一个采样频率,时域的横轴就可以换算成时间,频域的横轴就可以换算成频率。
DFT所做的只是选取了在给定长度内振动了整数个周期的正弦和余弦波作为基。
上面的信号是40个采样,如果给一个采样频率是100Hz,那么信号长度就是0.4s,原信号在40个采样内振动了两个周期,可以算出其频率为5Hz
对于频域,每个“在40个采样内振动了k个周期”的基信号的实际频率为 ,也就是说频域图中的每一个点代表2.5Hz,这个系统的频域分辨率是2.5Hz,所以我们从
得知原信号的实际频率为
FFT(Fast Fourier Transform)是什么
FFT是 Cooley & Tuket 两人1965年提出的快速计算DFT的算法。这背后还有个故事,美国和苏联1963年签了个核试验禁令,互相约定大家都不搞核试验了。但是美国不放心啊,怕毛子说一套做一套,肯尼迪就请了一堆科学家开会,说想搞一套不用去苏联检查就能探测到核试验的设备。美国在苏联周围放了一圈声波探测仪,但是遇到个问题——DFT算得太慢了。正巧 Tuket 和IBM的一个叫 Richard Garwin 的出席了那次会议。会上Garwin就想起来Tuket和Cooley好像合作过一个类似的算法,就让Tuket去找Cooley,但是隐藏了真实目的,告诉Cooley这个算法是为了探测氦3晶体的自旋周期……(也是醉了orz)。两人的算法最后于1965年发表,极大提升了DFT的计算速度,甚至被后世誉为“20世纪最伟大的工程算法”。
从DFT的公式可以看出其算法复杂度是 的,两人的FFT算法表明,在N为合数的情况下
,原长度为N的DFT可以分解为两个长度分别为
和
的DFT!
当然算法不停止于此,如果 和
还可以继续分解,就可以分解为更多小DFT来计算。
使用了算法中的分治(divide and conquer)策略。
而由于DFT的长度其实是可以任意选择的,因此通常在使用FFT时,都选择2的整数次幂作为FFT长度,这样可以一直分解到N/2个长度为2的DFT,复杂度直接降到
更详细的解答,请移步我做的视频:https://www.bilibili.com/video/av44600709
想从理论层面更深入了解DFT的,可以看我另外一个视频:https://www.bilibili.com/video/BV1a
0x00 写在前面
为了让更多人能够看到这个教程,希望大家收藏之前,也要点赞哦!!!蟹蟹大家的认可和鼓励。
傅里叶变换
快速傅里叶变换(Fast Fourier Transform,FFT)是一种可在 时间内完成的离散傅里叶变换(Discrete Fourier transform,DFT)算法。
在算法竞赛中的运用主要是用来加速多项式的乘法。
考虑到两个多项式 的乘积
,假设
的项数为
,其系数构成的
维向量为
,
的项数为
,其系数构成的
维向量为
。
我们要求 的系数构成的
维的向量,先考虑朴素做法。
可以用这段代码表示:
for ( int i = 0 ; i < n ; ++ i )
for ( int j = 0 ; j < m ; ++ j ) {
c [i + j] += a [i] * b [j] ;
}
思路非常清晰,其时间复杂度是 的。
所以我们来学习快速傅里叶变换。
0x01 关于多项式
多项式有两种表示方法,系数表达法与点值表达法
多项式的系数表示法
设多项式 为一个
次的多项式,显然,所有项的系数组成的系数向量
唯一确定了这个多项式。
多项式的点值表示法
将一组互不相同的 (叫插值节点)分别带入
,得到
个取值
.
其中
定理:
一个次多项式在
个不同点的取值唯一确定了该多项式。
证明:
假设命题不成立,存在两个不同的次多项式
,满足对于任何
,有
。
令,则
也是一个
次多项式。对于任何
,都有
。
即有
个根,这与代数基本定理(一个
次多项式在复数域上有且仅有
个根)相矛盾,故
并不是一个
次多项式,推到矛盾。
原命题成立,证毕。
如果我们按照定义求一个多项式的点值表示,时间复杂度为
已知多项式的点值表示,求其系数表示,可以使用插值。朴素的插值算法时间复杂度为 。
关于多项式的乘法
已知在一组插值节点 中
(假设个多项式的项数相同,没有的视为
)的点值向量分别为
,那么
的点值表达式可以在
的时间内求出,为
。
因为 的项数为
的项数之和。
设 分别有
项所以我们带入的插值节点有至少有
个。
如果我们能快速通过点值表式求出系数表示,那么就搭起了它们之间的一座桥了。
这也是快速傅里叶变换的基本思路,由系数表达式到点值表达式到结果的点值表达式再到结果的系数表达式。
0x02 关于复数的基本了解
我们把形如 这样的数叫做复数,复数集合用
来表示。其中
称为实部
,
称为虚部
,
为虚数单位,指满足
的一个解
;此外,对于这样对复数开偶次幂的数叫做虚数
.
每一个复数 都对应了一个平面上的向量
我们把这样的平面称为复平面
,它是由水平的实轴与垂直的虚轴建立起来的复数的几何表示。
故每一个复数唯一对应了一个复平面上的向量,每一个复平面上的向量也唯一对应了一个复数。其中 既被认为是实数,也被认为是虚数。
其中复数 的模长
定义为
在复平面的距离到原点的距离,
。幅角
为实轴的正半轴正方向(逆时针)旋转到
的有向角度。
由于虚数无法比较大小。复数之间的大小关系只存在等于与不等于两种关系,两个复数相等当且仅当实部虚部对应相等。对于虚部为 的复数之间是可以比较大小的,相当于实数之间的比较。
复数之间的运算满足结合律,交换律和分配律。
由此定义复数之间的运算法则:
复数运算的加法满足平行四边形法则,乘法满足幅角相加,模长相乘。
对于一个复数 ,它的共轭复数是
,
称为
的复共轭
.
共轭复数有一些性质
0x03 复数中的单位根
复平面中的单位圆
其中 单位根,表示为
,可知
(顺便一提著名的欧拉幅角公式 其实是由定义来的...)
将单位圆等分成 个部分(以单位圆与实轴正半轴的交点一个等分点),以原点为起点,圆的这
个
等分点为终点,作出
个向量。
其中幅角为正且最小的向量称为 次单位向量,记为
。
(有没有大佬帮我补张图啊,画不来)
其余的 个向量分别为
,它们可以由复数之间的乘法得来
。
容易看出 。
对于 ,它事实上就是
。
所以
关于单位根有两个性质
性质一(又称为折半引理):
证明一:
由几何意义,这两者表示的向量终点是一样的。
证明二:
由计算的公式:
其实由此我们可以引申出
性质二(又称为消去引理)
证明一:
由几何意义,这两者表示的向量终点是相反的,左边较右边在单位圆上多转了半圈。
证明二:
由计算的公式:
最后一步由三角恒等变换得到。
0x04 离散傅里叶变换(Discrete Fourier Transform)
首先我们单独考虑一个 项(
)的多项式
,其系数向量为
。我们将
次单位根的
~
次幂分别带入
得到其点值向量
。
这个过程称为离散傅里叶变换(Discrete Fourier Transform)。
如果朴素带入,时间复杂度也是 的。
所以我们必须要利用到单位根 的特殊性质。
对于
考虑将其按照奇偶分组
令
则可得到
分类讨论
设 ,
由上文提到的折半引理
对于
其中
由消去引理
故
注意, 与
取遍了
中的
个整数,保证了可以由这
个点值反推解出系数(上文已证明)。
于是我们可以知道
如果已知了 分别在
的取值,可以在
的时间内求出
的取值。
而 都是
一半的规模,显然可以转化为子问题递归求解。
时间复杂度:
0x05 离散傅里叶反变换(Inverse Discrete Fourier Transform)
使用快速傅里叶变换将点值表示的多项式转化为系数表示,这个过程叫做离散傅里叶反变换(Inverse Discrete Fourier Transform)。
即由 维点值向量
推出
维系数向量
。
设 为
得到的离散傅里叶变换的结果。
我们构造一个多项式
设向量 中
为
在
的点值表示
即 ,
我们考虑对 进行还原
于是
由和式的性质
令
对其进行化简
设
则
其公比为
当 即
时
此时
当 即
时
由等比数列求和公式
,此时
.
所以
将 带入原式
所以 .
其中 为原多项式
的系数向量
中的
.
由此得到:
对于多项式 由插值节点
做离散傅里叶变换得到的点值向量
。我们将
作为插值节点,
作为系数向量,做一次离散傅里叶变换得到的向量每一项都除以
之后得到的
就是多项式的系数向量
。
注意到 是
的共轭复数。
这个过程称为离散傅里叶反变换。
0x06 关于FFT在C++的实现
首先要解决复数运算的问题,我们可以使用C++STL自带的 依照精度要求
一般为
。
也可以自己封装,下面是我封装的复数类。
struct Complex {
double r, i ;
Complex ( ) { }
Complex ( double r, double i ) : r ( r ), i ( i ) { }
inline void real ( const double& x ) { r = x ; }
inline double real ( ) { return r ; }
inline Complex operator + ( const Complex& rhs ) const {
return Complex ( r + rhs.r, i + rhs.i ) ;
}
inline Complex operator - ( const Complex& rhs ) const {
return Complex ( r - rhs.r, i - rhs.i ) ;
}
inline Complex operator * ( const Complex& rhs ) const {
return Complex ( r * rhs.r - i * rhs.i, r * rhs.i + i * rhs.r ) ;
}
inline void operator /= ( const double& x ) {
r /= x, i /= x ;
}
inline void operator *= ( const Complex& rhs ) {
*this = Complex ( r * rhs.r - i * rhs.i, r * rhs.i + i * rhs.r ) ;
}
inline void operator += ( const Complex& rhs ) {
r += rhs.r, i += rhs.i ;
}
inline Complex conj ( ) {
return Complex ( r, -i ) ;
}
} ;
我们由上面的分析可以得到这个递归的写法。
bool inverse = false ;
inline Complex omega ( const int& n, const int& k ) {
if ( ! inverse ) return Complex ( cos ( 2 * PI / n * k ), sin ( 2 * PI / n * k ) ) ;
return Complex ( cos ( 2 * PI / n * k ), sin ( 2 * PI / n * k ) ).conj ( ) ;
}
inline void fft ( Complex *a, const int& n ) {
if ( n == 1 ) return ;
static Complex buf [N] ;
const int m = n >> 1 ;
for ( int i = 0 ; i < m ; ++ i ) {
buf [i] = a [i << 1] ;
buf [i + m] = a [i << 1 | 1] ;
}
memcpy ( a, buf, sizeof ( Complex ) * ( n + 1 ) ) ;
Complex *a1 = a, *a2 = a + m;
fft ( a1, m ) ;
fft ( a2, m ) ;
for ( int i = 0 ; i < m ; ++ i ) {
Complex t = omega ( n, i ) ;
buf [i] = a1 [i] + t * a2 [i] ;
buf [i + m] = a1 [i] - t * a2 [i] ;
}
memcpy ( a, buf, sizeof ( Complex ) * ( n + 1 ) ) ;
}
但是这样的 要用到辅助数组,并且常数比较大。
能不能优化呢?
我们把每一次分组的情况推演出来
递归分类的每一层
观察到每一个位置的数其实都是原来位置上的数的二进制后 位
了一下。
于是我们可以想,先将原数组调整成最底层的位置(很好调整吧)。
然后从倒数第二层由底向上计算。
这就是我们一般用来实现 的
算法。
考虑怎么合并?
在 算法中,合并操作被称作是蝴蝶操作。
虑合并两个子问题的过程,这一层有 项需要处理。假设
和
分别存在
和
中,
和
将要被存放在
和
中,合并的单位操作可表示为
只要将合并顺序换一下,再加入一个临时变量,合并过程就可以在原数组中进行。
令
合并过程如下:
。
至此,我们可以给出 算法的实现。
struct FastFourierTransform {
Complex omega [N], omegaInverse [N] ;
void init ( const int& n ) {
for ( int i = 0 ; i < n ; ++ i ) {
omega [i] = Complex ( cos ( 2 * PI / n * i), sin ( 2 * PI / n * i ) ) ;
omegaInverse [i] = omega [i].conj ( ) ;
}
}
void transform ( Complex *a, const int& n, const Complex* omega ) {
for ( int i = 0, j = 0 ; i < n ; ++ i ) {
if ( i > j ) std :: swap ( a [i], a [j] ) ;
for( int l = n >> 1 ; ( j ^= l ) < l ; l >>= 1 ) ;
}
for ( int l = 2 ; l <= n ; l <<= 1 ) {
int m = l / 2;
for ( Complex *p = a ; p != a + n ; p += l ) {
for ( int i = 0 ; i < m ; ++ i ) {
Complex t = omega [n / l * i] * p [m + i] ;
p [m + i] = p [i] - t ;
p [i] += t ;
}
}
}
}
void dft ( Complex *a, const int& n ) {
transform ( a, n, omega ) ;
}
void idft ( Complex *a, const int& n ) {
transform ( a, n, omegaInverse ) ;
for ( int i = 0 ; i < n ; ++ i ) a [i] /= n ;
}
} fft ;
注意代码中的 为
,而在代码中需要得到的是
。
因为 且
都是
的次幂,所以
,且
。
所以 (可以由折半引理证明)。
其余配图 代码都很好理解。
至此快速傅里叶变换就结束了。
0x07 写在后面
参考资料
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
2020-09-23 openssl cert
2020-09-23 git
2020-09-23 sha