时域的卷积 = 频域的乘法。这个定理是信号处理最重要的计算加速工具。

离散傅里叶变换(DFT)

DFT 把 N 个时域采样变换为 N 个频域系数:X[k] = Σ_{n=0}^{N-1} x[n] × e^{-2πi·kn/N}。朴素实现是 O(N²),因为每个频率系数需要与所有 N 个时域样本做点积。

package main

import (
    "fmt"
    "math"
    "math/cmplx"
)

// 朴素 DFT:O(N²)
func dft(x []float64) []complex128 {
    N := len(x)
    X := make([]complex128, N)
    for k := 0; k < N; k++ {
        for n := 0; n < N; n++ {
            angle := -2 * math.Pi * float64(k*n) / float64(N)
            X[k] += complex(x[n], 0) * cmplx.Exp(complex(0, angle))
        }
    }
    return X
}

// Cooley-Tukey FFT:O(N log N),N 必须是 2 的幂
func fft(x []complex128) []complex128 {
    N := len(x)
    if N <= 1 {
        return x
    }
    // 分治:偶数下标和奇数下标
    even := make([]complex128, N/2)
    odd := make([]complex128, N/2)
    for i := 0; i < N/2; i++ {
        even[i] = x[2*i]
        odd[i] = x[2*i+1]
    }
    E := fft(even)
    O := fft(odd)
    result := make([]complex128, N)
    for k := 0; k < N/2; k++ {
        t := cmplx.Exp(complex(0, -2*math.Pi*float64(k)/float64(N))) * O[k]
        result[k] = E[k] + t
        result[k+N/2] = E[k] - t
    }
    return result
}

func main() {
    // 合成信号:5Hz + 10Hz 正弦波
    N := 64
    signal := make([]float64, N)
    for i := range signal {
        t := float64(i) / float64(N)
        signal[i] = math.Sin(2*math.Pi*5*t) + 0.5*math.Sin(2*math.Pi*10*t)
    }

    // FFT
    cx := make([]complex128, N)
    for i, v := range signal { cx[i] = complex(v, 0) }
    X := fft(cx)

    // 找主要频率分量
    fmt.Println("主要频率分量(振幅 > 0.1N):")
    for k := 0; k < N/2; k++ {
        amp := cmplx.Abs(X[k])
        if amp > 0.1*float64(N) {
            fmt.Printf("  频率 %2d Hz: 振幅 = %.1f\n", k, amp/float64(N)*2)
        }
    }
}

卷积定理与音频处理

时域卷积 (f * g)[n] = Σ f[m]g[n-m] 的复杂度是 O(N²),但在频域只是点乘:FFT(f * g) = FFT(f) × FFT(g)。所以大卷积的算法是:FFT → 点乘 → IFFT,复杂度降到 O(N log N)。音频均衡器、图像模糊、CNN 的卷积层都用这个技巧。

package main

import (
    "fmt"
    "math"
    "math/cmplx"
)

// 用 FFT 做快速多项式乘法(卷积定理的应用)
// a = [1,2,3],b = [4,5,6],求 a*b 展开系数
func polyMul(a, b []float64) []float64 {
    // 补零到 2 的幂次
    n := 1
    for n < len(a)+len(b) { n <<= 1 }

    ca := make([]complex128, n)
    cb := make([]complex128, n)
    for i, v := range a { ca[i] = complex(v, 0) }
    for i, v := range b { cb[i] = complex(v, 0) }

    fa := fft(ca)
    fb := fft(cb)

    // 频域点乘
    fc := make([]complex128, n)
    for i := range fc { fc[i] = fa[i] * fb[i] }

    // IFFT(反转后 FFT 再除以 n)
    for i := 1; i < n/2; i++ { fc[i], fc[n-i] = fc[n-i], fc[i] }
    result := fft(fc)
    out := make([]float64, len(a)+len(b)-1)
    for i := range out {
        out[i] = math.Round(real(result[i])/float64(n)*10) / 10
    }
    return out
}

func fft(x []complex128) []complex128 {
    N := len(x)
    if N <= 1 { return x }
    even, odd := make([]complex128, N/2), make([]complex128, N/2)
    for i := 0; i < N/2; i++ { even[i] = x[2*i]; odd[i] = x[2*i+1] }
    E, O := fft(even), fft(odd)
    res := make([]complex128, N)
    for k := 0; k < N/2; k++ {
        t := cmplx.Exp(complex(0, -2*math.Pi*float64(k)/float64(N))) * O[k]
        res[k] = E[k] + t
        res[k+N/2] = E[k] - t
    }
    return res
}

func main() {
    // (1 + 2x + 3x²)(4 + 5x + 6x²) = 4 + 13x + 28x² + 27x³ + 18x⁴
    result := polyMul([]float64{1, 2, 3}, []float64{4, 5, 6})
    fmt.Println(result)  // [4 13 28 27 18]
}
算法时域运算频域运算总复杂度
朴素卷积O(N²)O(N²)
FFT 卷积O(N log N)O(N)O(N log N)
MP3 压缩丢弃高频分量10:1 压缩比
口诀时域复杂,频域简单——傅里叶变换是把难题搬到容易的空间里解。
推荐做法
  • 音频/图像处理优先考虑频域算法
  • FFT 输入长度填充到 2 的幂次,性能最优
  • 用 FFTW 或 gonum/dsp 等库而非手写
不推荐
  • 对非周期信号直接用 DFT——需要加窗函数减少频谱泄漏
  • 把 FFT 输出的 DC 分量(k=0)和信号频率混淆
常见误区
  • FFT 输出的后半段(k > N/2)是前半段的镜像(Nyquist 定理)

判断标准:能解释为什么 FFT 是 O(N log N) 而 DFT 是 O(N²) → 掌握本章。

傅里叶的洞见:任何信号都是正弦波的叠加。这一句话改变了整个现代通信。

— The Fourier Transform and Its Applications