FFT詳解及C語言實現

FFT詳解及C語言實現

DFT是幹嘛的?

​ 離散傅里葉變換(DFT),是傅里葉變換在時域和頻域上都呈現離散的形式,將時域信號的採樣變換爲在離散時間傅里葉變換(DTFT)頻域的採樣。在形式上,變換兩端(時域和頻域上)的序列是有限長的,而實際上這兩組序列都應當被認爲是離散週期信號的主值序列。即使對有限長的離散信號作DFT,也應當將其看作經過週期延拓成爲週期信號再作變換。在實際應用中通常採用快速傅里葉變換以高效計算DFT。 ——百度

FFT是幹嘛的?

​ 快速傅里葉變換 (fast Fourier transform), 即利用計算機計算離散傅里葉變換(DFT)的高效、快速計算方法的統稱,簡稱FFT。快速傅里葉變換是1965年由J.W.庫利和T.W.圖基提出的。採用這種算法能使計算機計算離散傅里葉變換所需要的乘法次數大爲減少,特別是被變換的抽樣點數N越多,FFT算法計算量的節省就越顯著。——百度

​ 在這裏我的理解就是:DFT將離散的時域信號轉化爲離散的頻域信號,而FFT就是在計算機上加速計算這一過程的算法的統稱。

直接計算DFT

首先我們看一下DFT變換對的公式

正變換:
Xk=DFT[x(n)]=n=0N1x(n)WNnk X(k)=DFT[x(n)]=\sum_{n=0}^{N-1}x(n)W_N^{nk}
反變換:
x(n)=IDFT[X(k)]=1Nk=0N1X(k)WNnk x(n)=IDFT[X(k)]=\frac{1}{N}\sum_{k=0}^{N-1}X(k)W_N^{-nk}
​ 可以很明顯的看出反變換相對於正變換來說只是符號不一樣以及多了一個比例因子,因爲DFT和IDFT的計算量及其的相似,所以只需以正變換爲例來考慮直接計算DFT時所存在的問題。

​ 如果直接的計算DFT,要計算離散頻譜X(k)的某一個值需要進行N次複數乘法與N-1次複數加法運算,因爲一共需要計算N個複數的值,所以總共要計算N×N次複數乘法和N×(N-1)次複數加法,當N的值非常大的時候N×(N-1)約等於N×N,因此1D的DFT原始算法的時間複雜度是 O(n2) ,對於2D的DFT其時間複雜度是 On4)…這個計算量是非常恐怖的。

舉個例子:計算序列{2,3,3,2}的DFT變換。
X[0]=2W40+3W40+3W40+2W40=10 X[0]=2W_4^{0}+3W_4^{0}+3W_4^{0}+2W_4^{0}=10

X[1]=2W40+3W41+3W42+2W43=1i X[1]=2W_4^{0}+3W_4^{1}+3W_4^{2}+2W_4^{3}=-1-i

X[2]=2W40+3W42+3W44+2W46=0 X[2]=2W_4^{0}+3W_4^{2}+3W_4^{4}+2W_4^{6}=0

X[3]=2W40+3W43+3W46+2W49=1+i X[3]=2W_4^{0}+3W_4^{3}+3W_4^{6}+2W_4^{9}=-1+i

​ 可以看出計算一個長度爲4的序列的DFT需要計算乘法16次,加法12次,當序列的長度非常大的時候計算量就非常大了,爲了加快這一過程,於是就產生了FFT。

FFT算法理解

​ 首先我們來看一下下面這個多項式,
f(x)=a0+a1x+a2x2+a3x3+....+anxn f(x)=a_0+a_1x+a_2x^2+a_3x^3+....+a_nx^n
​ 如果單純的直接計算,那麼時間複雜度爲 O(n2),那麼應該如何簡化這一個過程呢?這也可能就是傅里葉的無敵之處。

傅里葉說:
x=WNk x=W_N^k
可以簡化這個過程。

然後你會發現把上式帶入多項式中就得到了我們熟悉的這個東西。
f(WNk)=a0+a1WNk+a2WN2k+.....+anWNkn f(W_N^k)=a_0+a_1W_N^k+a_2W_N^{2k}+.....+a_nW_N^{kn}
是不是很陽朔,其實他就是DFT的公式。
Xk=DFT[x(n)]=n=0N1x(n)WNnk X(k)=DFT[x(n)]=\sum_{n=0}^{N-1}x(n)W_N^{nk}
再來講一下下面這個東西究竟爲何物
WNk W_N^k
在講之前補習一下複數的知識。

複數

我們把形如z=a+bi(a,b均爲實數)的數稱爲複數,其中a稱爲實部,b稱爲虛部,i稱爲虛數單位。當z的虛部等於零時,常稱z爲實數;當z的虛部不等於零時,實部等於零時,常稱z爲純虛數。複數域是實數域的代數閉包,即任何復係數多項式在複數域中總有根。 複數是由意大利米蘭學者卡當在十六世紀首次引入,經過達朗貝爾、棣莫弗、歐拉、高斯等人的工作,此概念逐漸爲數學家所接受。——百度

1、座標軸表示

在這裏插入圖片描述

x軸表示的是複數的實部a,y軸表示的是複數的虛部分b,他們的模長可以表示爲:
Z=a2+b2 |Z|=\sqrt{a^2+b^2}

2、複數的運算

設兩個複數分別爲Z1=a+bi,Z2=c+bi

Z1+Z2=(a+c)+(b+d)i Z_1+Z_2=(a+c)+(b+d)i

Z1Z2=(acbd)+(ad+bc)i Z_1Z_2=(ac-bd)+(ad+bc)i

乘法的極座標表示爲
(a1,θ1)(a2,θ2)=(a1a2,θ1+θ2) (a_1,\theta_1)*(a_2,\theta_2)=(a_1a_2,\theta_1+\theta_2)
可以看出複數相乘的實質就是:模長相乘,極角相加。(這個性質很重要)

旋轉因子的引入

​ 因爲在計算多項式的時候,如果暴力計算,
f(x)=a0+a1x+a2x2+a3x3+....+anxn f(x)=a_0+a_1x+a_2x^2+a_3x^3+....+a_nx^n
​ x的n次方會非常的難算,這該如何是好呢?
x0,x02,x03......x0n x_0,x_0^2,x_0^3......x_0^n
​ 我們可以引入一些特別的x,讓他們的若干次平方之後結果爲1

​ 首先我們可以想到1和-1滿足條件,1的多少次方都是1,-1的偶數次方是1,但是又出現了一個新的問題,那就是1和-1只有兩個數,而多項式需要的是n個不同的數。

​ 然後我們想到了複數,1,i,-1,-i,但是這也只有4個數,依然無法滿足條件。

​ 最後傅里葉引入了一個單位圓

在這裏插入圖片描述

這個圖網上盜來的,它把圓分成了8等份,對應着時域信號的8個離散數據和頻域的8個採樣點

然後把圓N等分(N是2的整數次冪)就得到了
WNk W_N^k
其中k就是圓的第k等份

由旋轉因子的對稱性、均勻性、週期性,我們可以整理出旋轉因子的兩個很重要的性質,
WNk=W2N2k W_N^k=W_{2N}^{2k}

WNk+π2=WNk W_N^{k+\frac{\pi}{2}}=-W_N^k

注意:這兩個性質很重要

帶入旋轉因子進行計算


x=WNk x=W_N^k
帶入多項式,變換成如下形式:
Ck=A0+A1(WNk)+A2(WNk)2+A3(WNk)3+......+An(WN1k)n1 C_k=A_0+A_1(W_N^k)+A_2(W_N^k)^2+A_3(W_N^k)^3+......+A_n(W_{N-1}^k)^{n-1}
將寄數項和偶數項分離:
Ck=(A0+A2(WNk)2+A4(WNk)4+.....+AN2(WNk)N2+WNk(A1+A3(WNk)2+A5(WNk)4+.....An1(WNk)n2) C_k=(A_0+A_2(W_N^k)^2+A_4(W_N^k)^4+.....+A_{N-2}(W_{N}^{k})^{N-2}+W_N^k(A_1+A_3(W_N^k)^2+A_5(W_N^k)^4+.....A_{n-1}(W_N^k)^{n-2})
然後
(WNk)2=WN2k=WN2k (W_N^k)^2=W_{N}^{2k}=W_{\frac{N}{2}}^k
劍上述結果帶入
Ck=(A0+A2WN2k+A4(WN2k)2+....+An2(WN2k)N21)+WNk(A1+A3WN2k+A5(WN2k)2+......An1(WN2k)N21) C_k=(A_0+A_2W_{\frac{N}{2}}^k+A_4(W_{\frac{N}{2}}^k)^2+....+A_{n-2}(W_{\frac{N}{2}}^k)^{\frac{N}{2}-1})+W_N^k(A_1+A_3W_{\frac{N}{2}}^k+A_5(W_{\frac{N}{2}}^k)^2+......A_{n-1}(W_{\frac{N}{2}}^k)^{\frac{N}{2}-1})
此時你會驚訝的發現:左邊Ak
Ak=A0+A2WN2k+A4(WN2k)2+.....+An2(WN2k)N21 A_k=A_0+A_2W_{\frac{N}{2}}^k+A_4(W_{\frac{N}{2}}^k)^2+.....+A_{n-2}(W_{\frac{N}{2}}^k)^{\frac{N}{2}-1}
就是多項式
A0+A2x+A4x2+.....+An2xN21 A_0+A_2x+A_4x^2+.....+A_{n-2}x^{\frac{N}{2}-1}
帶入了
x=WN2k x=W_{\frac{N}{2}}^k
的結果。

右邊Bk
Bk=A1+A3WN2k+A5(WN2k)2+......An1(WN2k)N21 B_k=A_1+A_3W_{\frac{N}{2}}^k+A_5(W_{\frac{N}{2}}^k)^2+......A_{n-1}(W_{\frac{N}{2}}^k)^{\frac{N}{2}-1}
就是多項式
A1+A3x+A5x2+.....+An1xN21 A_1+A_3x+A_5x^2+.....+A_{n-1}x^{\frac{N}{2}-1}
帶入了
x=WN2k x=W_{\frac{N}{2}}^k
的結果。

此時這兩個多項式恰好有都有N/2項,

把n個n次復根帶入一個長度爲n的多項式,是我們要做的DFT過程,那把n/2個 n/2次復根帶入一個長度爲n/2的多項式,是不是也可以看成是DFT?

依此類推:我們可以得到
Ck=Ak+WNkBk C_k=A_k+W_N^kB_k
但是此時0<= k <=N/2的,那另一半如何表示呢?

根據
WNk+π2=WNk W_N^{k+\frac{\pi}{2}}=-W_N^k

Ck=(A0+A2(WNk)2+A4(WNk)4+.....+AN2(WNk)N2+WNk(A1+A3(WNk)2+A5(WNk)4+.....An1(WNk)n2) C_k=(A_0+A_2(W_N^k)^2+A_4(W_N^k)^4+.....+A_{N-2}(W_{N}^{k})^{N-2}+W_N^k(A_1+A_3(W_N^k)^2+A_5(W_N^k)^4+.....A_{n-1}(W_N^k)^{n-2})
這個式子中,除了奇數次項提出的一個
WNk W_N^k
是奇數次冪,其他的都是偶數次冪,因此可以表示爲:
Ck+π2=AkWNkBk C_{k+\frac{\pi}{2}}=A_k-W_N^kB_k
此時0<= k <=N/2。

繼續遞歸即可,把一個長度爲N/2的多項式分成兩個長度爲N/4的多項式,然後DFT遞歸直到到多項式長度爲1爲止(因爲長度爲1的多項式只有常數項,變換結果就是其本身)。

以一個8點的離散數據點爲例
在這裏插入圖片描述
可以發現通過一次又一次的分割,最後把8點數據分成8個獨立的點,

它們分割後排序的二進制表示剛好就是沒分割前的二進制表示的相反結果

FFT蝶形的運算

其實上面說了那麼多,就是爲了搞出來這兩個東西
Ck=Ak+WNkBk C_k=A_k+W_N^kB_k

Ck+π2=AkWNkBk C_{k+\frac{\pi}{2}}=A_k-W_N^kB_k

首先以一個4點數據爲例(開始瘋狂盜圖)

在這裏插入圖片描述

然後再來一個8點的

計算過程
在這裏插入圖片描述
計算結果
在這裏插入圖片描述

C程序實現

1、定義複數的輸出函數

​ 已知複數是a+bi的形式,在程序中,爲了方便計算,將複數的實部和虛部分離開來進行計算,最後在輸出的時候在進行合併。

void output()
{
	int i;
	for(i=0;i<size;i++)
	{	
		printf("%.4f",x[i].real); //輸出複數的實部
		if(x[i].imag>=0.0001)
		{
			printf("+%.4fj\n",x[i].imag);  //當複數的虛補大於0.0001時,輸出+ 虛部 j的形式
		}
		else if(fabs(x[i].imag)<0.0001)
		{
			printf("\n");//當虛部小於0.001時,跳過虛部,不輸出
		}
		else
		{
			printf("%.4fj\n",x[i].imag);//上述兩個條件除外的形式,輸出 虛部 j的形式
		}
	}
}

2、數據點經過log(N)/log2級分割後重新排序

​ 一個N個數據長度的數據需要經過分割排序之後才能進行蝶形運算,引入上面的哪個圖,

在這裏插入圖片描述

發現沒有,在分割後,數據序列的位置發生了變化,而下面這個程序就是實現這一個過程。

void change()
{
	complex temp;
	unsigned short i=0,j=0,k=0;
	double t;
	for(i=0;i<size;i++)
	{
		k=i;
		j=0;
		t=(log(size)/log(2));  //算出序列的級數
		while( (t--)>0 )  //利用按位與以及循環實現碼位顛倒
		{
			j=j<<1;
			j|=(k & 1);
			k=k>>1;
		}
		if(j>i)    //將x(n)的碼位互換
		{
			temp=x[i];
			x[i]=x[j];
			x[j]=temp;
		}
	}
}

3、旋轉因子Wn的表示

我們知道旋轉因子在C語言中不好直接的表示出來,但是我們可以利用它的性質表示
WNk=ej2πkN=cos(2πkN)jsin(2πkN) W_N^k=e^{-j\frac{2\pi k}{N}}=cos(\frac{2\pi k}{N})-jsin(\frac{2\pi k}{N})
程序表示爲

void transform()
{
	int i;
	W=(complex *)malloc(sizeof(complex) * size);//給指針分配size的空間 size是數據長度
	for(i=0;i<size;i++)
	{
		W[i].real=cos(2*PI/size*i);  //歐拉表示的實部
		W[i].imag=-1*sin(2*PI/size*i);  //歐拉表示的虛部
	}
}

4、複數的加、減、乘運算

上面也講過複數的運算法則

加法:
Z1+Z2=(a+c)+(b+d)i Z_1+Z_2=(a+c)+(b+d)i
減法:
Z1Z2=(ac)+(bd)i Z_1-Z_2=(a-c)+(b-d)i
乘法:
Z1Z2=(acbd)+(ad+bc)i Z_1Z_2=(ac-bd)+(ad+bc)i
程序實現如下:

void add(complex a,complex b,complex *c)//定義結構體a、b和指針c    加法
{
	c->real=a.real+b.real; //c的目的是取出結構體中的數據
	c->imag=a.imag+b.imag;
}
void sub(complex a,complex b,complex *c)   //減法
{
	c->real=a.real-b.real;
	c->imag=a.imag-b.imag;
}
void mul(complex a,complex b,complex *c)   //乘法
{
	c->real=a.real*b.real - a.imag*b.imag;
	c->imag=a.real*b.imag + a.imag*b.real;
}

5、碟形運算

上面講過
Ck=Ak+WNkBk C_k=A_k+W_N^kB_k

Ck+π2=AkWNkBk C_{k+\frac{\pi}{2}}=A_k-W_N^kB_k

再來看8點蝶形運算的圖形

在這裏插入圖片描述

第一級:蝶形係數均爲
WN0 W_N^0
蝶形結點的距離爲1.

第二級:蝶形係數爲
WN0,WNN22 W_N^0,W_N^{\frac{N}{2^2}}
蝶形結點的距離爲2.

第三級:蝶形係數爲
WN0,WNN23,WN2N23,WN3N23 W_N^0,W_N^{\frac{N}{2^3}},W_N^{\frac{2N}{2^3}},W_N^{\frac{3N}{2^3}}
蝶形結點的距離爲4.

第log(N)/log2級:蝶形係數爲
WN0,WN1,......WNN21 W_N^0,W_N^1,......W_N^{\frac{N}{2}-1}
蝶形結點的距離爲N/2

void fft()
{
	int i=0,j=0,k=0,m=0;
	complex q,y,z;
	change();
	for(i=0;i<log(size)/log(2) ;i++)  //蝶形運算的級數
	{
		m=1<<i;   //移位 每次都是2的指數的形式增加,其實也可以用m=2^i代替
		for(j=0;j<size;j+=2*m)  //一組蝶形運算,每一組的蝶形因子乘數不同
		{
			for(k=0;k<m;k++)  //蝶形結點的距離  一個蝶形運算 每個組內的蝶形運算
			{
				mul(x[k+j+m],W[size*k/2/m],&q);
				add(x[j+k],q,&y);
				sub(x[j+k],q,&z);
				x[j+k]=y;
				x[j+k+m]=z;
			}
		}
	}
}

6、總體程序

#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#define N 1024
typedef struct{       //定義一個結構體表示覆數的類型
	double real;
	double imag;
}complex;
complex x[N], *W;   //定義輸入序列和旋轉因子
int size=0;   //定義數據長度
double PI=4.0*atan(1); //定義π 因爲tan(π/4)=1 所以arctan(1)*4=π,增加π的精度
void output()
{
	int i;
	for(i=0;i<size;i++)
	{	
		printf("%.4f",x[i].real);
		if(x[i].imag>=0.0001)
		{
			printf("+%.4fj\n",x[i].imag);
		}
		else if(fabs(x[i].imag)<0.0001)
		{
			printf("\n");
		}
		else
		{
			printf("%.4fj\n",x[i].imag);
		}
	}
}
void change()
{
	complex temp;
	unsigned short i=0,j=0,k=0;
	double t;
	for(i=0;i<size;i++)
	{
		k=i;
		j=0;
		t=(log(size)/log(2));
		while( (t--)>0 )
		{
			j=j<<1;
			j|=(k & 1);
			k=k>>1;
		}
		if(j>i)
		{
			temp=x[i];
			x[i]=x[j];
			x[j]=temp;
		}
	}
	output();
}
void transform()
{
	int i;
	W=(complex *)malloc(sizeof(complex) * size);
	for(i=0;i<size;i++)
	{
		W[i].real=cos(2*PI/size*i);
		W[i].imag=-1*sin(2*PI/size*i);
	}
}
void add(complex a,complex b,complex *c)
{
	c->real=a.real+b.real;
	c->imag=a.imag+b.imag;
}
void sub(complex a,complex b,complex *c)
{
	c->real=a.real-b.real;
	c->imag=a.imag-b.imag;
}
void mul(complex a,complex b,complex *c)
{
	c->real=a.real*b.real - a.imag*b.imag;
	c->imag=a.real*b.imag + a.imag*b.real;
}
void fft()
{
	int i=0,j=0,k=0,m=0;
	complex q,y,z;
	change();
	for(i=0;i<log(size)/log(2) ;i++)
	{
		m=1<<i;
		for(j=0;j<size;j+=2*m)
		{
			for(k=0;k<m;k++)
			{
				mul(x[k+j+m],W[size*k/2/m],&q);
				add(x[j+k],q,&y);
				sub(x[j+k],q,&z);
				x[j+k]=y;
				x[j+k+m]=z;
			}
		}
	}
}
int main()
{
	int i;
	printf("輸入數據個數\n");
	scanf("%d",&size);//輸入數據的長度(2的整數次冪)
	printf("輸入數據的實部、虛部\n");
	for(i=0;i<size;i++)
	{
		scanf("%lf%lf",&x[i].real,&x[i].imag);  //輸入複數的實部和虛部
	}
	printf("輸出倒序後的序列\n");
	transform();//變換序列順序
	fft();//蝶形運算
	printf("輸出FFT後的結果\n");
	output();//輸出結果
	return 0;
}

輸出結果演示:

依然以序列{2,3,3,2}爲例

在這裏插入圖片描述

之前計算步驟如下:
X[0]=2W40+3W40+3W40+2W40=10 X[0]=2W_4^{0}+3W_4^{0}+3W_4^{0}+2W_4^{0}=10

X[1]=2W40+3W41+3W42+2W43=1i X[1]=2W_4^{0}+3W_4^{1}+3W_4^{2}+2W_4^{3}=-1-i

X[2]=2W40+3W42+3W44+2W46=0 X[2]=2W_4^{0}+3W_4^{2}+3W_4^{4}+2W_4^{6}=0

X[3]=2W40+3W43+3W46+2W49=1+i X[3]=2W_4^{0}+3W_4^{3}+3W_4^{6}+2W_4^{9}=-1+i

可以非常明顯的看出程序運算結果正確。

總結

本次的C程序還有一些不夠完善的地方,現在還只能簡單的輸入一個2的整數次冪長度的序列,下次將補充:輸入一個連續的時域信號,經過FFT變換後,輸出結果,並將結果用gunplot畫出來。爭取達到matlab中的fft函數效果。

再次強調:由於本人實在太菜,如果有什麼錯誤或者不清楚的地方還請各位大佬指出!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章