본문으로 건너뛰기
김신건의 로그

시간 복잡도 (Time Complexity)

· 수정 · 📖 약 5분 · 1,658자/단어 #algorithm #foundation #complexity #big-o #analysis
time complexity, 시간 복잡도, time-complexity, Big-O, 빅오, 점근적 표기법, asymptotic notation

정의

시간 복잡도 (Time Complexity) 는 입력 크기 N 이 증가할 때 알고리즘의 연산 횟수 증가율 을 나타낸다. PS 에서는 주로 최악 케이스 Big-O 표기법 을 사용.

한 줄 요약: “N 이 충분히 클 때 실행 시간이 얼마나 빠르게 늘어나는가”.

점근적 표기법

세 가지 표기법이 있으며, 각각 상한, 양면 한계, 하한 을 나타낸다.

Big-O (상한, Upper Bound)

“충분히 큰 n 에서 f(n) 이 c·g(n) 보다 크지 않다”. PS 에서 가장 많이 쓰는 표기법.

Big-Omega (하한, Lower Bound)

“f(n) 이 c·g(n) 보다 항상 크다”. 알고리즘의 최선 케이스 또는 문제의 최적 하한 표현.

Big-Theta (양면 한계, Tight Bound)

“f(n) 이 g(n) 과 동일한 성장률”. 마스터 정리 에서 Θ 를 사용.

IMPORTANT

PS 에서 “O(N log N) 알고리즘”이라고 할 때 엄밀히는 Θ(N log N) 을 의미하는 경우가 많다. 상한만 말하는 게 아님.

시각화

복잡도 성장 비교

flowchart LR
    A["O(1)<br/>해시 조회, 배열 접근"]
    B["O(log N)<br/>이분 탐색, 균형 트리"]
    C["O(N)<br/>선형 탐색, 누적 합"]
    D["O(N log N)<br/>병합 정렬, 힙 정렬"]
    E["O(N^2)<br/>버블 정렬, 나이브 DP"]
    F["O(2^N)<br/>부분집합, 백트래킹"]
    G["O(N!)<br/>순열 완전 탐색"]
    A --> B --> C --> D --> E --> F --> G

오른쪽으로 갈수록 빠르게 증가. N=20 기준으로 O(2^20) = 1,048,576, O(20!) = 2.4×10^18.

대회 제한별 허용 복잡도

flowchart LR
    N["입력 N"]
    N --> L1["N 12이하: O(N!) 허용"]
    N --> L2["N 25이하: O(2^N) 허용"]
    N --> L3["N 500이하: O(N^3) 허용"]
    N --> L4["N 5000이하: O(N^2) 허용"]
    N --> L5["N 30만이하: O(N log N) 허용"]
    N --> L6["N 10^8이하: O(N) 허용"]

대표 복잡도별 예시

복잡도N=10N=100N=10^4N=10^6대표 알고리즘
O(1)1111배열 접근, 해시 조회
O(log N)371320이분 탐색, 균형 트리
O(N)1010010^410^6선형 탐색, 누적 합
O(N log N)336641.3×10^52×10^7병합 정렬, 힙 정렬
O(N^2)10010^410^810^12버블 정렬, 삽입 정렬
O(N^3)1,00010^610^12-플로이드-워셜, 나이브 행렬곱
O(2^N)1,02410^30--부분집합, 백트래킹
O(N!)3.6×10^610^157--순열 완전 탐색

PS 에서는 “1초 = 약 10^8 ~ 10^9 연산” 을 기준으로 사용.

대회 제한과 실처리량

10^8 연산/초 를 기준으로 한 허용 N 크기:

제한 시간O(N!)O(2^N)O(N^3)O(N^2)O(N log N)O(N)O(log N)
1초N<=12N<=25N<=500N<=5,000N<=3×10^5N<=10^8N<=10^30
2초N<=12N<=26N<=630N<=7,000N<=6×10^5N<=2×10^8N<=10^30

IMPORTANT

이 수치는 개략 가이드. 연산 종류 (나눗셈 vs 덧셈), 캐시 효율, 상수 인자, 언어 차이 (C++ vs Python vs Java) 에 따라 실제 처리량이 크게 다르다. Python 은 C++ 대비 50~100배 느리다고 가정해야 한다.

C++ 에서 단순 덧셈 루프는 10^9 연산/초도 가능하지만, 해시 테이블 조회나 포인터 추적은 10^7 ~ 10^8 수준. 메모리 접근 패턴이 성능에 큰 영향.

Amortized 복잡도

분할 상환 분석 (Amortized Analysis) 은 개별 연산이 아닌 N번 연산의 총 비용 / N 으로 평균 복잡도를 구한다.

대표 예: 동적 배열 push_back

  • capacity 1에서 시작, push_back N번:

    • 재할당 시점: 1, 2, 4, 8, …, N
    • 복사 비용 합계: 1 + 2 + 4 + … + N = 2N - 1 = O(N)
    • 일반 삽입 비용: N번 × O(1) = O(N)
    • 총합: O(N) → 1회 평균 O(1)
  • 개별 push_back 은 재할당 시 O(N) 일 수 있지만, N번 실행 시 평균 O(1).

다른 예: 병합 정렬 은 모든 단계에서 O(N) 비용이 log N 번 → O(N log N).

복잡도 분석 절차

실전에서 알고리즘 복잡도를 빠르게 분석하는 절차:

1. 루프 구조 파악
   - 단일 루프 O(1~N): 대부분 O(N)
   - 이중 루프: O(N^2) (단, 내부가 분기로 O(1) 이면 O(N))
   - log 분할 (이분 탐색 패턴): O(log N)

2. 재귀 점화식 파악
   - T(n) = 2T(n/2) + O(n): 마스터 정리 -> O(N log N)
   - T(n) = T(n-1) + O(1): O(N)
   - T(n) = 2T(n-1) + O(1): O(2^N)

3. 자료구조 연산 비용 더하기
   - Q번 세그트리 쿼리: O(Q log N)
   - N번 정렬 후 Q번 이분 탐색: O(N log N + Q log N)

구현

복잡도별 연산 횟수를 직접 계산해 비교한다.

// 복잡도별 연산 횟수 비교
#include <bits/stdc++.h>
using namespace std;
int main() {
  long long n;
  cin >> n;

  // floor(log2(n)) 계산
  long long logN = 0, tmp = n;
  while (tmp > 1) { tmp /= 2; logN++; }

  cout << "N = " << n << "\n";
  cout << "O(1):       " << 1LL << " ops\n";
  cout << "O(log N):   " << logN << " ops\n";
  cout << "O(N):       " << n << " ops\n";
  cout << "O(N log N): " << n * logN << " ops\n";
  cout << "O(N^2):     " << n * n << " ops\n";

  return 0;
}
stdin
1000
결과
N = 1000
O(1):       1 ops
O(log N):   9 ops
O(N):       1000 ops
O(N log N): 9000 ops
O(N^2):     1000000 ops

함정

WARNING

상수 인자 무시: O(N) 이라도 상수가 크면 O(N log N) 보다 느릴 수 있다. 예: 캐시 miss 가 잦은 O(N) vs 캐시 친화적인 O(N log N). 이론 복잡도만으로 판단하지 말 것.

WARNING

최악 vs 평균 케이스 혼동: 퀵 정렬은 평균 O(N log N) 이지만 최악 O(N^2). 대회에서 최악 케이스 입력이 들어오면 TLE. 최악 케이스 보장이 필요하면 병합 정렬 또는 힙 정렬.

CAUTION

Python 의 실제 속도: Python 은 C++ 대비 50100배 느리다. O(N^2) 알고리즘이 C++ 에서 1초면 Python 에서 50100초. 대회에서 Python 사용 시 복잡도를 한 단계 낮춰야 한다.

WARNING

해시 테이블 최악 케이스: unordered_map 은 평균 O(1) 이지만 최악 O(N). 해킹 테스트 케이스에서 TLE 발생 가능. reserve() + custom hash 또는 map (O(log N)) 으로 대체.

IMPORTANT

Amortized 와 Worst-case 구분: vector::push_back 은 amortized O(1) 이지만 개별 호출은 O(N) 일 수 있다. 실시간 응답이 필요한 시스템에서는 amortized 복잡도가 충분하지 않을 수 있다.

WARNING

재귀 깊이 O(N): 재귀 함수가 O(N) 깊이로 호출되면 스택 오버플로우 위험. C++ 기본 스택은 약 1~8 MB. N=10^5 수준의 재귀는 반복문으로 변환하거나 스택 크기를 늘려야 한다.

BOJ 연습 문제

번호제목설명
BOJ 2750수 정렬하기O(N^2) vs O(N log N) 비교
BOJ 1920수 찾기O(N) 선형 탐색 vs O(log N) 이분 탐색
BOJ 11650좌표 정렬하기O(N log N) 정렬
BOJ 10815숫자 카드이분 탐색 O(log N)

관련 위키

이 글의 용어 (7개)
마스터 정리 (Master Theorem)algorithm
정의 마스터 정리 (Master Theorem) 는 분할 정복 알고리즘의 시간 복잡도를 표현하는 점화식을 닫힌 형태 (closed form) 로 풀어주는 공식입니다. 적용 대상:…
분할 정복 (Divide and Conquer)algorithm
정의 분할 정복 (Divide and Conquer) 은 문제를 더 작은 부분 문제로 나누고 (Divide), 각각 해결한 후 (Conquer), 결과를 합쳐 (Combine) …
세그먼트 트리 (Segment Tree)algorithm
정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
이분 탐색 (Binary Search)algorithm
정의 이분 탐색 (Binary Search) 은 정렬된 시퀀스에서 목표값의 위치를 O(log N) 에 찾는 알고리즘. 매 단계에서 후보 구간을 절반으로 줄인다. 탐색이 본질이 아…
자료구조 (Data Structures)algorithm
정의 자료구조 (Data Structure) 는 데이터를 효율적으로 저장하고 접근하기 위한 조직화 방법. 문제 풀이에서는 시간 복잡도와 공간 복잡도의 trade-off 를 정확히…
정렬 알고리즘algorithm
정의 정렬 (sort) 은 원소들의 컬렉션을 어떤 전순서 (total order) 기준으로 재배열하는 것. 알고리즘 입문의 정석 주제이자, 데이터베이스·검색·통계 등 모든 시스템…
Merge Sortalgorithm
정의 Merge Sort (병합 정렬) 는 분할 정복 (Divide & Conquer) 으로 동작하는 비교 정렬. 배열을 반으로 나눠 각각 정렬한 뒤, 두 정렬된 부분을 병합 (…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기