포스트

복잡도 분석 — Big-O, 점근 표기, 그리고 분할상환이라는 거짓말 같은 진실

Big-O와 점근 분석으로 알고리즘 성능을 입력 크기 기준으로 가늠하는 법. O·Ω·Θ의 차이, 최선·평균·최악, 동적 배열이 왜 분할상환 O(1)인지까지 애니메이션으로 풀어봅니다.

복잡도 분석 — Big-O, 점근 표기, 그리고 분할상환이라는 거짓말 같은 진실

왜 “초”가 아니라 “n에 대한 함수”로 재나

알고리즘이 빠른지 느린지를 스톱워치로 재면 안 됩니다. 같은 코드도 CPU·언어·캐시 상태에 따라 측정값이 출렁이고, 무엇보다 입력이 커질 때 어떻게 변하는지를 말해주지 못합니다. 우리가 정말 알고 싶은 건 “지금 1만 건에서 0.1초인데, 1억 건이 되면 어떻게 되나?”입니다.

그래서 알고리즘의 비용을 입력 크기 n에 대한 함수로 표현하고, n이 충분히 커질 때의 증가율(growth rate) 만 봅니다. 상수 배수와 낮은 차수 항을 버리는 이 추상화가 바로 점근 분석(asymptotic analysis) 이고, 그 표기법이 Big-O입니다.

증가율이 전부다 — 같은 n에서도 운명이 갈린다

아래는 입력 n이 커질 때 연산 횟수가 어떻게 벌어지는지를 실제로 달리게 한 것입니다. 처음엔 다 비슷해 보이지만, n이 커질수록 곡선의 등급(class)이 모든 것을 결정합니다.

입력 크기 n → 연산 횟수 ↑ O(2ⁿ) O(n²) O(n log n) O(n) O(log n) O(1)

핵심은 n이 작을 때의 순간이 아니라, n이 커질 때의 기울기입니다. O(2ⁿ)은 처음엔 바닥을 기다가 n이 30만 되어도 천장을 뚫습니다. 입력 100만 건 기준으로 감을 잡아봅시다.

등급이름n=10n=1,000n=1,000,000대표 예
O(1)상수111배열 인덱스 접근, 해시 조회
O(log n)로그31020이진 탐색, 균형 트리
O(n)선형101,0001,000,000배열 1회 순회
O(n log n)선형로그3310,0002천만비교 정렬
O(n²)제곱100100만1조이중 루프, 버블 정렬
O(2ⁿ)지수1,02410³⁰⁰우주가 끝남부분집합 완전탐색

O(n²)와 O(n log n)의 차이는 “조금 느리다”가 아니라, 100만 건에서 1조 번 vs 2천만 번 — 5만 배입니다. 알고리즘 선택이 하드웨어 업그레이드를 압도하는 이유입니다.

O, Ω, Θ — 위·아래·정확히

Big-O는 사실 상한(upper bound) 만 말합니다. 엄밀히는 세 가지가 있습니다.

  • O(f(n)) (빅오): “아무리 나빠도 이보다 빠르다” — 상한.
  • Ω(f(n)) (빅오메가): “아무리 좋아도 이보다 느리다” — 하한.
  • Θ(f(n)) (빅세타): 상한과 하한이 같을 때 — 정확한 차수.

수식으로는, 어떤 상수 $c>0$와 $n_0$가 존재해서 $n \ge n_0$인 모든 $n$에 대해

\[0 \le f(n) \le c \cdot g(n) \implies f(n) \in O(g(n))\]

이 성립하면 됩니다. 즉 상수 배수와 작은 항을 무시한다는 약속입니다. $3n^2 + 100n + 7$은 그냥 $O(n^2)$입니다 — n이 커지면 $n^2$ 항이 지배하니까요.

일상 대화에선 “퀵정렬은 O(n log n)”이라고 하지만, 엄밀히는 평균 $\Theta(n\log n)$, 최악 $O(n^2)$입니다. “이 알고리즘은 O(n²)야”라는 말이 최악인지 평균인지를 구분하는 습관이 면접과 장애 대응을 가릅니다.

최선·평균·최악 — 어느 것을 말하고 있나

같은 알고리즘도 입력에 따라 비용이 다릅니다. 해시 테이블 조회는 평균 O(1) 이지만 충돌이 몰리면 최악 O(n) 입니다. 시스템을 설계할 때 중요한 건 보통 최악과 평균 둘 다입니다 — 평균이 좋아도 최악이 사용자 한 명의 요청을 1초간 멈추게 한다면 SLO를 깹니다.

flowchart LR
    I["입력 분포"] --> B["최선 Ω<br/>운 좋을 때"]
    I --> A["평균 Θ<br/>보통"]
    I --> W["최악 O<br/>적이 노릴 때"]
    W --> S["SLO·타임아웃 설계는<br/>최악을 기준으로"]

악의적 입력으로 최악을 유도하는 것이 바로 알고리즘 복잡도 공격입니다(예: 의도적 해시 충돌로 서버를 O(n²)에 빠뜨리는 HashDoS). “평균만 보면 된다”가 통하지 않는 영역입니다.

분할상환(amortized) — 가끔 비싼데 평균은 싸다

여기서 가장 오해가 많은 개념. 동적 배열(Java ArrayList, C++ vector, Python list)에 원소를 n번 추가한다고 합시다. 보통은 빈 자리에 그냥 넣어 O(1). 그런데 꽉 차면 2배 크기의 새 배열을 만들어 전부 복사합니다 — 그 순간만 O(n).

그럼 추가 1회는 O(n)일까요? 아닙니다. 드물게 일어나는 비싼 복사 비용을 전체 횟수로 나눠 갚으면(amortize), 추가 1회의 평균은 O(1) 입니다. 아래에서 직접 보세요 — 대부분 싸게 들어가다, 용량이 차는 순간(빨강)에만 전체를 복사하고 용량이 2배로 점프합니다.

동적 배열 (용량이 차면 2배로 재할당) 추가 1회 비용 복사! 복사! 평균 = O(1) 비싼 복사를 전체로 나눠 갚음

왜 정확히 O(1)일까요? 1, 2, 4, 8, …, n으로 2배씩 늘리면 총 복사 비용은 $1 + 2 + 4 + \dots + n < 2n = O(n)$입니다. 이걸 n번의 추가로 나누면 추가당 평균 O(1). 핵심은 “2배(곱셈) 로 늘린다”는 것 — 만약 매번 +1씩(덧셈) 늘리면 복사 총합이 $O(n^2)$가 되어 추가당 O(n)으로 망합니다.

이것이 ArrayList가 capacity를 1.5~2배로 키우는 이유입니다. 분할상환 O(1) 는 “매 연산이 O(1)”이 아니라 “충분히 많이 하면 1회 평균이 O(1)“이라는, 미묘하지만 결정적인 약속입니다.

분할상환을 분석하는 방법은 세 가지입니다.

기법아이디어
집계법(aggregate)n번 연산 총비용을 구해 n으로 나눔
회계법(accounting)싼 연산에 “요금”을 미리 적립해 비싼 연산 때 인출
잠재법(potential)자료구조의 “위치 에너지” Φ로 비용 변화를 추적

시간만이 아니다 — 공간, 그리고 캐시라는 현실

복잡도는 시간(time) 과 공간(space) 둘 다 있습니다. 메모이제이션은 시간을 사서 공간을 쓰는(동적계획법의 핵심) 전형적 트레이드오프입니다.

그리고 Big-O가 숨기는 상수가 현실에서 결정적일 때가 있습니다. 같은 O(n)이라도 메모리를 연속으로 훑는 배열 순회는 캐시 친화적이라 빠르고, 포인터를 따라 메모리를 점프하는 연결 리스트 순회는 캐시 미스로 수십 배 느릴 수 있습니다. 점근적으로 같아도 상수 인자가 다릅니다.

1
2
배열:      [a][b][c][d]  → 한 캐시라인에 여러 원소, 예측 가능한 prefetch
연결리스트: a → ? → ? → ? → 매번 다른 메모리, 캐시 미스 폭발

그래서 실무 규칙: 먼저 점근 등급을 맞추고(O(n²)→O(n log n)), 그 다음 같은 등급 안에서 상수와 캐시를 줄입니다. 순서를 거꾸로 하면(작은 상수에 집착하며 나쁜 등급을 방치) 입력이 커질 때 무너집니다.

프로덕션에서 마주치는 함정

함정증상해법
숨은 O(n) 안의 루프“리스트인데 in 으로 멤버십 검사” → 전체 O(n²)set/map(해시) O(1) 조회로
문자열 += 누적불변 문자열을 n번 이어붙여 O(n²)StringBuilder/join
N+1 쿼리1건마다 쿼리 1번 → DB 왕복 O(n)일괄 조회(IN)·조인
최악 무시평균 O(1)인데 HashDoS로 O(n²)랜덤 시드 해시, 트리화(Java 8 HashMap)
조기 최적화측정 없이 상수 깎기먼저 프로파일링 → 등급부터

면접/리뷰 단골 질문

  • Q. O와 Θ의 차이? → O는 상한(“이보다 빠르다”), Θ는 상한·하한이 같은 정확한 차수. 퀵정렬은 평균 Θ(n log n), 최악 O(n²).
  • Q. 동적 배열 추가가 O(1)인 이유? → 분할상환. 2배씩 늘리면 총 복사 O(n)을 n번으로 나눠 추가당 평균 O(1). +1씩 늘리면 O(n)으로 망함.
  • Q. O(n log n)과 O(n²) 중 작은 n에선 후자가 빠를 수도 있나? → 가능. 상수가 작으면 작은 n에서 역전(그래서 퀵정렬도 작은 구간은 삽입정렬로 전환). 하지만 n이 커지면 등급이 이긴다.
  • Q. 평균이 좋으면 최악은 무시해도 되나? → 아니다. SLO·타임아웃은 최악 기준. 악의적 입력(복잡도 공격)이 최악을 노린다.
  • Q. 같은 O(n)인데 왜 속도 차이? → 숨은 상수와 캐시 지역성. 배열 순회 vs 연결리스트 순회.

정리

  • 알고리즘 비용은 초가 아니라 입력 n에 대한 증가율로 잰다 — 상수·낮은 차수는 버리는 점근 분석.
  • O(상한)·Ω(하한)·Θ(정확) 를 구분하고, 최선·평균·최악 중 무엇을 말하는지 늘 의식한다.
  • 분할상환은 “가끔 비싼 연산”을 전체로 나눠 갚아 평균 비용을 낮추는 분석 — 동적 배열의 2배 증가가 대표.
  • 등급을 먼저 맞추고, 같은 등급 안에서 상수·캐시 지역성을 줄인다. Big-O가 숨기는 현실을 잊지 말 것.

이 글은 「알고리즘 A-Z」 시리즈의 출발점입니다. 다음 글부터 배열·연결 리스트를 시작으로 자료구조와 알고리즘을 하나씩, 왜 그렇게 설계됐는지와 함께 파고듭니다.

</content> </invoke>

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

시리즈 · 알고리즘 A-Z (20편)

  1. 복잡도 분석 — Big-O, 점근 표기, 그리고 분할상환이라는 거짓말 같은 진실 지금 보는 글
  2. 배열과 연결 리스트 — 같은 O(n)인데 왜 한쪽이 수십 배 느린가
  3. 스택·큐·힙 — 접근 규칙 하나가 만드는 강력함
  4. 해시 테이블 — 평균 O(1)의 비밀과 그 평균이 깨지는 순간
  5. 비교 정렬 — 퀵·머지·힙, 그리고 Ω(n log n)이라는 넘을 수 없는 벽
  6. 선형 시간 정렬 — 비교를 버리면 Ω(n log n) 벽이 사라진다
  7. 이진 탐색 — 반씩 버리는 마법, 그리고 "답을 이분하라"
  8. 재귀와 분할정복 — 콜스택에 쌓이는 신뢰, 마스터 정리로 푸는 비용
  9. 이진 탐색 트리와 균형 — 한쪽으로 쏠리는 트리를 회전으로 바로 세우기
  10. 그래프 순회 — BFS의 물결, DFS의 깊이, 그리고 위상정렬
  11. 최단 경로 — 다익스트라의 확신, A*의 직감, 벨만-포드의 인내
  12. 최소 신장 트리와 Union-Find — 가장 싸게 잇기, 그리고 거의 O(1)의 마법
  13. 동적계획법 — 같은 계산을 두 번 하지 않는다는 단 하나의 원칙
  14. 그리디 — 눈앞의 최선만 집었는데 전체가 최적일 때
  15. 문자열 매칭 — 어긋났을 때 얼마나 똑똑하게 다시 시작하는가
  16. 확률적 자료구조 — 정확함을 메모리와 맞바꾸는 기술 (Bloom·HyperLogLog·Count-Min)
  17. 일관된 해싱 — 노드가 들고날 때 데이터를 통째로 옮기지 않는 법
  18. 합의 알고리즘 — 죽는 서버들 사이에서 하나의 진실을 정하는 법 (Raft·Paxos)
  19. 레이트 리미팅과 스트리밍 알고리즘 — 폭주를 막고, 끝없는 흐름에서 표본을 뽑다
  20. 의미로 검색하다 — 벡터 검색(ANN·HNSW)과 PageRank, 그리고 시리즈의 끝