【转】用C语言实现FFT算法
傅里叶变换
快速傅里叶变换(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 ( int ) * ( 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 ( int ) * ( 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 写在后面
感谢
的blog让我学会了FFT。
感谢
的讲解让我再次理解了FFT。
参考资料
转发自知乎:https://zhuanlan.zhihu.com/p/31584464
【转】用C语言实现FFT算法的更多相关文章
- 用C实现FFT算法
用C语言编写FFT算法 转http://blog.sina.com.cn/s/blog_65d639d50101buo1.html #include "math.h" #defi ...
- FFT算法
FFT算法的完整DSP实现 傅里叶变换或者FFT的理论参考: [1] http://www.dspguide.com/ch12/2.htm The Scientist and Engineer's G ...
- FFT算法的完整DSP实现(转)
源:FFT算法的完整DSP实现 傅里叶变换或者FFT的理论参考: [1] http://www.dspguide.com/ch12/2.htm The Scientist and Engineer's ...
- FFT算法的完整DSP实现
傅里叶变换或者FFT的理论参考: [1] http://www.dspguide.com/ch12/2.htm The Scientist and Engineer's Guide to Digita ...
- 10个经典的C语言面试基础算法及代码
10个经典的C语言面试基础算法及代码作者:码农网 – 小峰 原文地址:http://www.codeceo.com/article/10-c-interview-algorithm.html 算法是一 ...
- 数据结构C语言版 弗洛伊德算法实现
/* 数据结构C语言版 弗洛伊德算法 P191 编译环境:Dev-C++ 4.9.9.2 */ #include <stdio.h>#include <limits.h> # ...
- 快速傅立叶变换(FFT)算法
已知多项式f(x)=a0+a1x+a2x2+...+am-1xm-1, g(x)=b0+b1x+b2x2+...+bn-1xn-1.利用卷积的蛮力算法,得到h(x)=f(x)g(x),这一过程的时间复 ...
- msp430学习笔记-实现开方log等计算及FFT算法(待续)
MSP430 FFT算法实现 http://bbs.21ic.com/icview-391532-1-1.html http://blog.sina.com.cn/s/blog_6cd2030b010 ...
- 2维FFT算法实现——基于GPU的基2快速二维傅里叶变换
上篇讲述了一维FFT的GPU实现(FFT算法实现——基于GPU的基2快速傅里叶变换),后来我又由于需要做了一下二维FFT,大概思路如下. 首先看的肯定是公式: 如上面公式所描述的,2维FFT只需要拆分 ...
随机推荐
- CSS3 clip裁剪动画
CSS3 clip裁剪动画 下面是比较简单的例子 <pre><html><head><style type="text/css">i ...
- 用 PHP 函数变量数组改变代码结构
项目越做越大,代码越来越乱,维护困难.原因很多吧.起初为了实现功能,并没有注重代码的结构,外包公司嘛.虽然公司的项目负责人一直考虑复用.封装,但是我觉得基本上没有达到想要的效果.因为整个代码中没有用到 ...
- vs2017离线包下载安装并且不占用C盘空间使用教程
安装vs2017,前提是你的环境是.NET4.6,VS2017在下载好安装程序安装的时候,会根据你选择的功能模块来下载所需要的安装程序,微软.安卓和苹果等平台的SDK.模拟器和第三方扩展功能等会在用户 ...
- C# 学习笔记 多态(一)虚方法
在面对对象编程中,类的三大特性分别为封装,继承,多态.其中多态的具体实现,依赖于三个方法,也就是虚方法,抽象类和接口. 多态的具体作用是什么呢?或者说多态的存在有什么意义呢?多态的存在有效的降低了程序 ...
- APS.NET MVC + EF (06)---模型
在实际开发中,模型往往被划分为视图模型和业务模型两部分,视图模型靠近视图,业务模型靠近业务,但是在具体编码上,它们之间并不是隔离的. 6.1 视图模型和业务模型 模型大多数时候都是用来传递数据的.然而 ...
- C#中全局作用域的常量、字段、属性、方法的定义与使用
场景 在开发中,经常会有一些全局作用域的常量.字段.属性.方法等. 需要将这些设置为全局作用域保存且其实例唯一. 注: 博客主页: https://blog.csdn.net/badao_liuman ...
- HTML5中localStorage的使用
为什么要存在localStorage 在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cook ...
- webpack4 plugins 篇
demo 代码点此,篇幅有限,仅介绍几个常用的. start 什么是 plugins ? While loaders are used to transform certain types of mo ...
- Ubuntu19.04安装常用软件
安装Indicator Stickynotes 桌面便签小工具sudo add-apt-repository ppa:umang/indicator-stickynotessudo apt-get u ...
- PMP备考-第二章-项目运行环境与项目经理
组织系统的三大因素:组织治理框架,管理要素和组织结构 组织治理和项目治理 组织治理 :组织中的重要决策制定框架,谁有权在什么时候用什么发放做出并推行什么重要决策. 项目治理 :组织为项目建立的高级别的 ...