快速傅里叶变换(FFT)算法是我学习算法过程中碰到过最难的骨头之一, 看了不少教材,听过不少视频,第一眼看上去就感觉FFT面目狰狞,不是平常人轻易能靠近的算法,更不说如何运用了。那些术语我就不在此强调了,反正我多数也没搞懂。我在此试图给出一种平易近人的解释法,一是要PK一下EEers的这份解释,同时希望对大家学习有用。也许我这种解释法可称为从CS的角度。
首先,FFT有什么用?要解释这个本身就不平易近人。如果把FFT算法的目的简单地描述成只是为了高效计算多项式可以吗,比如,多项式的乘积、加法等。所谓多项式,形如 c = c0 + c1*x + c2*x^2,ci是系数,x是变量。当然, 我们可以(通常也必须)考虑次数更高的多项式,比如一个n阶多项式,c = c0 + c1*x + c2*x^2 + … + cn*c^n。两个多项式相乘用算法实现容易吗?当然容易!我想,初中生都会。两次n比特的整数进行乘法运算需要多少次比特操作?这个……进一步,在计算机实现中,我们考察运算效率的时候发现,中学数学的方法可以进一步优化。
题外话,多项式的运算有什么意思?举一个我们应该最熟悉的例子,如果我们把二进制比特串看为一个多项式,那么这种多项式的运算就有意思了,对吧?比如,1100可看为1*x^3 + 1*x^2 + 0 + 0 。当然FFT的作用远非如此平淡,有科学家形容,FFT是梦幻般的算法。
回到正题,简化的第一步思路就是如何正确(达到高效的正确)地表达多项式。我们知道,n+1个点可以决定一条n阶曲线。比如,我们熟知的两点决定一条直线(一阶多项式)。换而言之,给定多项式系数c0…cn,我只需要得到求出这个多项式曲线上的n+1个点值,那这就是这条多项式的一种表达。注意,此时我们得到的是n+1个值。
如果我们给定一个x值,求出所有x^i的值(i从0到n),那么我们只需要用系数去乘这些值再累加就可以得到一个曲线上的点。如何我们有n+1个x,就可以求n+1个多项式上的值,就确定了一条曲线。
例子:考虑多项式c3*x^3 + c2*x^2 + c1*x + c0 。分别取x为1和-1,得到两个点c3+c2+c1+c0,c3+c2-c1+c0。再取x等于-2和2,又可以得到两个点。四个点可以决定这一多项式。注意,这里是举例子,实际中我们并不这样取值,比如我们不取2和-2,因为这达不到提高效率的效果,到底取什么见后分解。我们先看一下以上过程用矩阵的表达方式:
1 x0 x0^2 x0^2
1 x1 x1^2 x1^3
1 x2 x2^2 x2^3
1 x3 x3^2 x3^3
我们把上面这个矩阵记为F4,把系数(c0,c1,c2,c3)记为列向量c,所得结果其实是向量d = F4*c 。希望这些简单的线性代数知识不要为难到你们。提醒一下,最左边一列可看成是xi^0。至此,我们得到了什么呢?一个多项式的表达d而已。拓展思维,如果有两个多项式分别表示为d和d’,这两个家伙相乘怎么做?d = d×d’ = (F4*c)×(F4*c’),前一个括号用了n^2次乘法,后一个括号用了n^2次乘法,然后是向量乘×用若干个n的运算(用×是想区别这两种不同的“乘法”,在此不多解释,直观上理解即可。),加起来是一个O(n^2)。这里如果不理解Big O表示也没关系,大概只需要知道是n^2乘上一个常数那么多步骤就好。
一个关键点就是,要选什么样的x来进行计算?本质上讲,任意的x都应该可以,只是说到快速算法,我们就必须有所选择了。我们熟知的一个定理(代数基本定理)告诉我们,一个n阶多项式有n个解,比如x^2 + 2x + 1 = 0有2个解。如果是z^n = 1,那么就有n个解。要交待的是,这里的解必须允许有复数解。对于n-1阶多项式,FFT算法再次很特殊地选择了z^n = 1的解来作为多项式中x的取值,这种特殊性使得所构造出的Fn矩阵具有非常优秀的特性。
以下图示表示了z^8 = 1所有解的情况。
直观上可以这样理解z^n = 1 的解,就是把单位圆上分成n份,从实数轴开始旋转(360/8)度角,所落在单位圆上的点就是解。希望你没有被那一系列的w吓到,无非就是一堆数字,它们客观地存在则而已。我们不用担心这些数字(如果对这些知识不熟悉,可参考这个平易近人的解释。),要思考的是这些数字带给我们什么样的好处。我们把这里的解带入之前构造的F4中去,得到这样的F4。注意,z^4 = 1有四个解,分别是1、-1、i和-i。此时,取这四个解带入上面的矩阵中(即x0 =1,x1=i,x2=-1,x3=-i),得到这样的F4(记w = i):
1 1 1 1 1 1 1 1
1 w w^2 w^3 1 i -1 -i
1 w^2 w^4 w^6 ===== 1 -1 1 -1
1 w^3 w^6 w^9 1 -i -1 i
w是单位圆上夹角为90度的那个点。w^2只是角度再旋转了90度。在此感谢泰勒、感谢欧拉、感谢高斯。但是,没关系,我们完全可以暂时忘记其中大部分的内涵。只要从这个矩阵中,我们至少能发现,某些运算是重复的。如果能合理地利用这种重复,我们就可以简化运算。请注意:w^2、w^3和w^6都出现了两次。w^9在图中是没有的,因为这里是周期函数,w^9 == w。
看到这里(害怕复数且编程从来没有用过复数的)大家会问(也许我会问),我们只考虑实系数的多项式,你给我弄出这么一堆乱七八糟的复数,我怎么用啊?请注意一点,我们只是通过某种方式把多项式表达成某个(复数)矩阵与向量的乘积,然后借助矩阵的运算我们得到了效率。到最后,我们只需要把某个矩阵的乘积通过逆计算,恢复出实数多项式的系数,我们不就成功了吗?为何要纠结过程当中用到了复数?所以,这个问题就应该是,我们能否从这一堆矩阵乘积得到的值中恢复出某个多项式。问题其实就是,如果d = F4*c,给出d能否求出c。啊哈,简单的线性代数知识,如果这个F4是个可逆矩阵,为何不可以呢?F4是可逆的吗?
好吧,如果讲点线性代数的知识的话,我们知道F4是对称矩阵,然后balabala,我们知道,原来F4的逆矩阵是1/4*(F的共轭矩阵 )。好,如果不理解,只需要记得结论,傅里叶矩阵是可以求逆的,而且非常容易。这也可以让我们很放心地去使用。至少我们明白,傅里叶矩阵的构造是冲某些重要性质来的,存在可逆矩阵就是其中之一。
经常讲解FFT的教材中会说,FFT可以把任意曲线表示为正弦波的叠加,或者用正弦波拟合任意曲线。通过d = Fn*c这个式子也基本可以理解这样的表述。只需要把Fn里面的所有w^i理解为正弦波就好了。本质上就是如此。
好了,至此我基本解释完,但是一个关键问题还没有解释。我们如何提高了效率?其实之前还没有讨论这个问题,我只是说某些操作看上去是重复的,可以利用。要考察FFT的效率,还得从傅里叶矩阵F说起。
简单来说,一个F可以唯一分解为这样的三部分,比如F4可以分解为两个F2的某种表达,一般化就是Fn 为 两个Fn/2的某种表达。这种表达当然并不难,考虑到本文强调思路强调容易,就不具体展开。当然,通过对多项式的变形,同样可以得到相同的结论。视乎你从什么角度去看这个算法。然后利用算法的知识我们知道,计算效率递归公式为:
T(n) = 2T(n/2) + O(n)
直观上理解就是一个n次多项式的求解是两个n/2次多项式的求解再加上若干个n那么多的操作。所以,效率是O(n*log n)。对数以2为底。与之前的n^2相比,这是一次伟大的进步。不难看出,从我们CSers应该熟知的算法设计的角度去看,这不就是所谓的“分治法”嘛,最基本的算法设计策略,也许ACMers会对这样Naive的策略显出不屑,不知道……
例子:如果n=1024,n^2 = 1000,000。n*log n = 1024*10 。很明显的改进。
拓展思考:FFT在计算机安全学的哪个领域被用到?
本文的贡献在于把FFT讲解中吓人的频域、时域、偶数部分、奇数部分、蝶形算法等等给简化掉,试图抽取最重要的部分呈现,如果有一点点帮助,则善莫大焉。纯属个人见解,且学识有限,错误难免,敬请原谅。所有的贡献归于前人,所有的错误都是我个人做出,不代表本单位水平和观点。
FFT源代码
1、大型软件可以考虑FFTW. 似乎我选择推荐它是因为它源自于MIT.
2、追求简单可以选择KissFFT.
3、JJ的Ugly page,实在太牛了,他写了一本书Algorithms for Programmers,维护了一个FFT的主页。
4、Sage也是开源软件,当然,我们可以直接使用它进行FFT计算,这是一份参考文档。
以下代码是对网络上某段代码的简单改写,原来出处不可考。
//
// 快速傅立叶变换 Fast Fourier Transform
// By
// 2014-09-18
// 版本 1.0
// 实现了《算法导论》的迭代FFT算法,
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
const int N = 1024;
const float PI = 3.1416;
//计算2的n次幂
int pow2(int n)
{
int res = 1;
for(int i = 0; i < n; i++)
{
res = res*2;
}
return res;
}
//交换a与b的数值
inline void swap(float &a, float &b)
{
float t;
t = a;
a = b;
b = t;
}
//判断输入整数x是否2的指数.
int is_power_of_two(int x)
{
while (x > 1)
{
if (x % 2)
return 0;
x /= 2;
}//end for while
return 1;
}
//计算输入值的比特长度
static int log_2(int n)
{
int res;
for (res = 0; n >= 2; res++)
n = n >> 1;
return res;
}//end of log2
//对比特串进行比特反向转置,比如,0001变成1000,1010变成0101
//输入两个值,第一个是需要转置的值,第二个是值的比特长度,需要逆向的真实长度而不是类型长度.
int bitrev(int inp, int numbits)
{
int i, rev = 0;
for (i = 0; i < numbits; i++)
{
rev = (rev << 1) | (inp & 1);
inp >>= 1;
}
return rev;
}
//比特反向转置,以bitrev的顺序重新排列输入的数组,n是一个2的指数值。
void bitrev_permute(float xreal [], float ximag [], int n)
{
int len_n;
len_n = log_2(n);
for (int i = 0; i < n; i++)
{
int bri = bitrev(i, len_n);
//跳过已经交换过的数值
if (bri <= i) continue;
swap (xreal [i], xreal [bri]);
swap (ximag [i], ximag [bri]);
}//end for
}//bitrev_permute
//迭代FFT,源自《算法导论》, lbwang codes it on 2014.0917 morning.
//输入值为两个数组,分别表示输入系数的实部、虚部,n是数组大小
//输出值存放在原数组中
void iterative_FFT(float xreal [], float ximag [], int n)
{
float treal, timag, ureal, uimag, theta, phi, wreal, wimag;
for(int s = 1; s < log_2(n) + 1; s++)
{
int m = pow2(s);
theta = 2 * PI / m; //每次增加theta度
//printf("theta is %8.4f\n", theta);
for(int k = 0; k < n; k = k + m)
{
phi = 0; //初始角为0
wreal = 1.0; //cos(phi)
wimag = 0.0; // sin(phi)
for(int j = 0; j < m/2; j++)
{
//t <- w*A[k + j + m/2]
//注意这里的乘法规则:(a+bi) * (c+di ) = (ac - bd) + (bc +ad) i
treal = wreal * xreal[k + j + m/2] - wimag * ximag[k + j + m/2];
timag = wreal * ximag[k + j + m/2] + wimag * xreal[k + j + m/2];
//u <- A[k + j]
ureal = xreal[k + j];
uimag = ximag[k + j];
//A[k+j] = u + t
xreal[k + j] = ureal + treal;
ximag[k + j] = uimag + timag;
//for debgu
//printf("Got a ximag as %8.4f", ximag[k + j]);
//A[k + j +m/2] = u-t
xreal[k + j + m/2] = ureal - treal;
ximag[k + j + m/2] = uimag - timag;
//for debgu
//printf("Got a ximag as %8.4f", ximag[k + j + m/2]);
//w = w*w_m,意味着w增加theta角,因为 w_m = e^i*(theta);
phi = phi + theta;
wreal = cos(phi);
wimag = sin(phi);
//for debug
//printf("Phi, wreal and wimag is %8.4f %8.4f %8.4f \n", phi,wreal,wimag);
}
}
}
//printf("One computation is finish.\n");
}
//测试函数,自动生成2^n个n比特二进制值,进行FFT计算,然后输出到文本output中。
void iFFT_test ()
{
char outputfile [] = "output.txt"; // 将结果输出到文件 output.txt 中
char inputfile [] = "input.txt";
char tempfile [] = "temp.txt";
float xreal [N] = {}, ximag [N] = {};
int n = 4, i;
FILE *input, *output, *temp;
//创建输出文件
if (!(output = fopen (outputfile, "w")))
{
printf ("Cannot open file. ");
exit (1);
}
//创建输出文件
if (!(input = fopen (inputfile, "w")))
{
printf ("Cannot open file. ");
exit (1);
}
//迭代测试,穷举所有n比特数;
for(int i = 0; i< pow2(n) ; i++)
{
//产生测试数据,本质上是形如0000-1111 的二进制比特
int b = i;
for(int j = 0; j < n; j++)
{
xreal[j] = b & 1;
ximag[j] = 0.0 ;
b >>= 1;
}
//输入值输出到input.txt文件中去
fprintf (input, "i real imag \n");
for (int i = 0; i < n; i ++)
fprintf (input, "%4d %8.4f %8.4f \n", i, xreal [i], ximag [i]);
fprintf (input, "================================= \n");
//对特定的值进行iFFT计算
iterative_FFT(xreal, ximag, n);
//结果输出到output文件中
fprintf (output, "i real imag \n");
for (int i = 0; i < n; i ++)
fprintf (output, "%4d %8.4f %8.4f \n", i, xreal [i], ximag [i]);
fprintf (output, "================================= \n");
}//end of the iterative computing.
if ( fclose (output))
{
printf ("File close error. ");
exit (1);
}
if ( fclose (input))
{
printf ("File close error. ");
exit (1);
}
}
//主函数
int main ()
{
iFFT_test ();
return 0;
}