시간복잡도와 공간복잡도
알고리즘이 입력 크기 n에 대해 얼마나 시간이 걸리고, 얼마나 메모리를 쓰는지를 함수로 표현한 척도입니다.
왜 필요한가?
같은 문제를 푸는 두 코드:
[ 코드 A ] [ 코드 B ]
이중 루프로 중복 검사 정렬 후 인접 비교
n=10 → 100번 n=10 → 30번
n=1,000 → 1,000,000번 n=1,000 → 10,000번
n=1,000,000 → 1조 번 n=1,000,000 → 약 2천만 번 O
→ "동작한다"와 "쓸 만하다"는 다르다
→ 입력이 커질 때 어떻게 증가하는가? = 복잡도
머신 성능, 언어, 컴파일러 최적화는 모두 상수에 흡수됩니다. 우리가 보는 건 입력 n에 대한 증가율입니다.
점근 표기법
O(f(n)) 상한 — 아무리 나빠도 이만큼
Ω(f(n)) 하한 — 아무리 잘해도 이만큼
Θ(f(n)) 상·하한 일치 — 정확히 이 차수
| 표기 | 의미 | 실무 사용 |
|---|---|---|
| Big-O | 최악의 경우 상한 | 가장 흔히 사용 |
| Big-Ω | 최선의 경우 하한 | 이론에서 사용 |
| Big-Θ | 상한 = 하한 | 타이트한 분석 |
코딩테스트·면접에서 "복잡도가 어떻게 돼?"는 거의 항상 Big-O를 묻는 것입니다.
표기 규칙
3n² + 5n + 100 → O(n²) // 최고차항만 남긴다
1000n → O(n) // 상수 계수 무시
log₂ n, log₁₀ n → O(log n) // 로그 밑 무시 (상수배 차이)
자주 보는 시간복잡도
빠름 ────────────────────────────────────→ 느림
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)
| 복잡도 | 이름 | 대표 예시 |
|---|---|---|
| O(1) | 상수 | 배열 인덱스 접근, 해시 조회 |
| O(log n) | 로그 | 이진 탐색, 균형 BST |
| O(n) | 선형 | 단순 순회, 선형 탐색 |
| O(n log n) | 선형로그 | 병합/퀵/힙 정렬 |
| O(n²) | 이차 | 이중 루프, 버블/선택/삽입 정렬 |
| O(2ⁿ) | 지수 | 부분집합 완전탐색, 단순 피보나치 재귀 |
| O(n!) | 팩토리얼 | 순열 생성, 외판원 완전탐색 |
시간 감각 (1초 ≈ 10⁸ 연산)
n ≤ 10 → O(n!) 가능 (순열)
n ≤ 20 → O(2ⁿ) 가능 (비트마스크)
n ≤ 5,000 → O(n²) 안전
n ≤ 100,000 → O(n²) 빠듯, 보통 통과
n ≤ 1,000,000 → O(n log n) 까지 가능
n ≥ 10⁷ → O(n) 또는 O(log n) 만 가능
코딩테스트에서 N 제한을 보면 알고리즘이 보입니다. 거의 항상 맞습니다.
시간복잡도 예제
O(n) — 선형
int sum(int[] arr) {
int total = 0;
for (int x : arr) total += x; // n번 반복
return total;
}
O(n²) — 이중 루프
boolean hasDuplicate(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] == arr[j]) return true;
}
}
return false;
}
O(log n) — 이진 탐색
int binarySearch(int[] arr, int target) {
int lo = 0, hi = arr.length - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
탐색 범위가 매 반복마다 절반:
n → n/2 → n/4 → ... → 1
반복 횟수 = log₂ n
공간복잡도
추가로 사용하는 메모리 양을 입력 크기 n의 함수로 표현합니다. 입력 자체가 차지하는 공간은 보통 제외하고, 알고리즘이 추가로 쓰는 공간만 봅니다.
| 공간복잡도 | 예시 |
|---|---|
| O(1) | 변수 몇 개만 사용 (in-place 알고리즘) |
| O(log n) | 이진 탐색의 재귀 호출 스택 |
| O(n) | 배열·해시맵에 결과 누적, 깊이 n 재귀 |
| O(n²) | DP 2차원 테이블, 인접 행렬 |
시간 vs 공간 트레이드오프
[ 메모이제이션(DP) ]
시간 줄임 ↔ 공간 더 씀
[ in-place 정렬 ]
공간 줄임 ↔ 구현 복잡 ↑
→ 문제 제약(메모리 제한 256MB 등)을 보고 선택
헷갈리기 쉬운 포인트
재귀의 공간복잡도
재귀 함수는 호출 스택도 공간을 차지
depth=n 재귀 → 공간 O(n)
큰 입력에서 스택 오버플로 위험
평균 vs 최악
| 알고리즘 | 평균 | 최악 |
|---|---|---|
| 퀵소트 | O(n log n) | O(n²) (정렬된 입력 + 끝 피벗) |
| 해시맵 조회 | O(1) | O(n) (모든 키 같은 버킷) |
| 이진 탐색 트리 | O(log n) | O(n) (편향 트리) |
면접은 보통 최악, 실무는 평균이 더 중요한 경우도 많음.
분할상환(Amortized) 분석
ArrayList.add():
보통 → O(1)
배열 확장 시 → O(n) (두 배로 늘리며 복사)
n번 연산을 평균내면:
총 비용 ≈ 2n
연산당 → O(1)
→ 분할상환 O(1)
실전 체크리스트
문제를 풀기 전에:
1. 입력 크기 N 확인 → 허용 가능한 복잡도 추정
2. 시간 제한 확인 (1초≈10⁸) → O(n²)? O(n log n)?
3. 메모리 제한 확인 (256MB) → 배열 크기 계산
4. 알고리즘 결정 → 복잡도 검산 → 구현
복잡도를 미리 계산하지 않고 구현부터 시작하면, 다 짜놓고 시간 초과로 갈아엎습니다.
핵심 정리
시간복잡도와 공간복잡도는 알고리즘이 입력 크기 n에 대해 자원을 얼마나 쓰는지를 표현하는 척도입니다. 머신 성능 같은 상수는 무시하고 증가율만 봅니다. Big-O는 최악 시나리오의 상한을 나타내며, 실무·코딩테스트의 표준 표기입니다. 자주 보는 차수는 O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!) 순이고, N 제한을 보면 가능한 알고리즘이 거의 결정됩니다. 공간복잡도는 추가 메모리 사용량이며, 시간과 트레이드오프 관계가 자주 나타납니다.