어떤 알고리즘이 입력 크기 N에 따라 얼마나 많은 계산을 하는지 대략 나타낼 때 O(⋅) 표기를 쓴다.
DFT를 그대로 계산하면 모든 출력 k마다 모든 입력 n을 확인한다. 그래서 대략 다음만큼 계산한다.
N⋅N=N2
이것을 O(N2)이라고 쓴다.
FFT는 문제를 반씩 나누어 다시 합친다. N을 계속 반으로 나누면 몇 번 만에 1이 되는지 나타내는 수가 log2N이다.
8→4→2→1
위 과정은 3번 나누었으므로
log28=3
이다. FFT의 계산량은 대략 다음처럼 줄어든다.
O(NlogN)
여기서 logN은 보통 log2N처럼 “몇 번 반으로 나눌 수 있는가”라는 뜻으로 생각해도 충분하다.
Big-O와 점화식으로 보는 차이
O(⋅) 표기는 정확한 실행 시간을 말하는 것이 아니라, 입력 크기가 커질 때 지배적으로 커지는 항을 나타내는 표기다. 예를 들어
3N2+10N+7
은 N이 커질수록 N2 항이 가장 중요하므로 O(N2)이라고 쓴다.
DFT를 직접 계산하면 출력 X[k]가 N개 있고, 각 출력마다 입력 x[n]을 N개 모두 더한다.
k의개수N⋅n의개수N=N2
반면 FFT는 길이 N 문제를 길이 N/2 문제 두 개로 나누고, 마지막에 N개 정도의 값을 합친다. 이를 점화식으로 쓰면
T(N)=2T(N/2)+O(N)
재귀 단계별로 보면 더 분명하다. r번째 단계에는 작은 문제가 2r개 있고, 각각의 크기는 2rN이다. 그 단계에서 합치는 총량은 대략
2r⋅2rN=N
으로 일정하다.
한 단계의 총 합치는 비용은 N, 단계 수는 log2N이므로 전체는
O(NlogN)
이 된다. 여기서 중요한 것은 “반으로 나누기”만으로 충분하지 않다는 점이다. 작은 문제의 결과를 다시 쓸 수 있는 단위원근의 대칭이 있어야 이 점화식이 성립한다.
직관 비유
토너먼트 대진표를 생각하자. 16명이 참가하면 한 라운드가 끝날 때마다 남은 사람 수가 16명, 8명, 4명, 2명, 1명으로 반씩 줄어든다. 우승자가 나오기까지 필요한 라운드 수가 log216=4이고, 로그는 이렇게 “몇 번 반으로 줄이면 하나가 되는가”를 세는 말이다.
예제
N=16이면 반으로 나누는 과정은 다음과 같다.
16→8→4→2→1
따라서
log216=4
그냥 DFT 계산량과 FFT식 계산량을 거칠게 비교하면
N2=162=256
이고,
Nlog2N=16⋅4=64
이다. 실제 알고리즘에는 숨은 상수가 있지만, 입력이 커질수록 차이가 빠르게 벌어진다.
손풀이 체크
32를 계속 반으로 나누면 몇 번 만에 1이 될까?
답 보기
5번
log28은?
답 보기
3
N=8일 때 N2과 Nlog2N은 각각 얼마일까?
답 보기
64와 24
다음으로 이어지는 생각
FFT가 빠른 이유는 단순히 반으로 나누기 때문만은 아니다. 단위원 위 회전값들이 서로 짝을 이루는 성질도 함께 사용한다.
이번 장에서 기억할 3문장
O 표기는 입력이 커질 때 계산량이 어떤 항에 지배되는지 나타낸다.
log2N은 N을 1이 될 때까지 몇 번 반으로 나눌 수 있는지를 센다.
FFT의 속도 이득은 단계마다 전체 O(N) 일을 하고 그런 단계가 logN개인 구조에서 나온다.
C++ Practice
C++로 확인하기
O(N2)와 O(NlogN)의 증가 속도 비교하기
#include <cmath>#include <iomanip>#include <iostream>usingnamespace std;
intmain() {
double N =1024;
double slow = N * N;
double fast = N *log2(N);
cout << fixed <<setprecision(0);
cout <<"N^2 = "<< slow <<"\n";
cout <<"N log2 N = "<< fast <<"\n";
cout <<"ratio = "<< slow / fast <<"\n";
}