시간/공간 복잡도
시간/공간 복잡도(Big-O 표기법)는 입력 크기 n이 커질수록 실행 시간이나 메모리 사용량이 얼마나 빠르게 늘어나는지를 나타내는 표기법입니다. 정확한 초 단위가 아니라 "커지는 추세(패턴)"에 집중한다는 점이 핵심입니다.
핵심 개념
O(1) — 상수 시간
입력 크기와 상관없이 항상 일정한 횟수만 연산합니다. 예: 배열에서 인덱스로 값 하나를 바로 꺼내는 연산.
핵심 개념
O(log n) — 로그 시간
매 단계마다 처리할 범위를 절반씩 줄여나갈 때 나타납니다. 예: 다음 토픽에서 다룰 이진 탐색.
핵심 개념
O(n) — 선형 시간
입력 전체를 한 번씩은 훑어야 하는 경우입니다. 예: 배열을 처음부터 끝까지 순회하며 최댓값을 찾는 연산.
핵심 개념
O(n²) — 이차 시간
입력의 모든 쌍을 서로 비교해야 할 때 나타납니다. 예: 이중 for문, 버블 정렬(자료구조-09에서 다룸).
핵심 개념
O(2ⁿ) — 지수 시간
매 단계마다 선택지가 두 배씩 늘어날 때 나타납니다. 예: 캐시 없이 재귀로 계산하는 피보나치(자료구조-10에서 다룬 완전 탐색).
핵심 개념
시간·공간 트레이드오프
복잡도는 실행 시간뿐 아니라 메모리 사용량도 같은 방식(O(1), O(n)...)으로 표현합니다. 메모이제이션은 시간을 아끼는 대신 공간을 더 쓰는 대표적인 예입니다.
빅오가 필요한 이유와 표기의 뜻
알고리즘을 비교할 때 실제 걸린 시간은 컴퓨터와 언어마다 달라서 기준이 되기 어렵습니다. 그래서 입력 크기 n에 대해 연산 횟수가 어떤 함수로 늘어나는지를 보는 점근 표기를 씁니다. 교재(CLRS)에서 O는 증가율의 상한, Ω는 하한, Θ는 같은 증가율로 위와 아래를 모두 막는 경우를 뜻합니다. 보통 말하는 '빅오'는 이 중 O입니다.
빅오를 구하는 규칙
3n² + 5n + 7은 n이 커질수록 n² 항이 지배하므로 O(n²)입니다. 상수 3도 증가율을 바꾸지 않아 버립니다.
숫자로 확인하기
자주 하는 오해
j = 1부터 j *= 2로 커지면 안쪽은 약 log n번이어서 전체는 O(n log n)입니다. 안쪽이 i번만 돌면 1 + 2 + … + n = n(n+1)/2 이므로 이 경우는 O(n²)입니다.
면접에서 이렇게 물어요
면접 질문
O(log n)이 되는 대표적인 예는 무엇인가요?
범위를 매번 절반으로 줄이는 이진 탐색입니다. 정렬된 배열에서 n이 두 배가 되어도 비교가 한 번 늘 뿐입니다.
면접 질문
해시 테이블 조회의 시간 복잡도는 얼마인가요?
해시 함수가 키를 고르게 흩뜨린다는 가정 아래 평균적으로 O(1)입니다. 충돌이 심하면 한 버킷에 몰려 최악에는 O(n)까지 나빠질 수 있습니다.
면접 질문
재귀로 피보나치를 구하는 코드의 복잡도는 얼마인가요?
캐시 없이 fib(n-1) + fib(n-2)를 부르면 같은 값을 반복해서 계산합니다. 호출 횟수는 정확히는 황금비 약 1.618의 n제곱에 비례하지만, O(2ⁿ)도 올바른 상한입니다. 메모이제이션을 쓰면 O(n)이 됩니다.
면접 질문
비교 기반 정렬은 왜 O(n log n)보다 빠를 수 없나요?
n개를 비교만으로 정렬하려면 가능한 순서가 n!가지이므로 결과를 가려내는 데 최악의 경우 최소 log₂(n!) = Ω(n log n)번의 비교가 필요하다고 알려져 있습니다. 그래서 병합·힙 정렬은 이 하한에 닿는 알고리즘입니다.
실제 시스템에서는
get·set은 상수 시간이고, 끝에 추가하는 add는 분할 상환 상수 시간입니다. 맨 앞에 넣으면 뒤의 원소를 모두 밀어야 해서 원소 수에 비례하는 시간이 듭니다.
get과 put이 상수 시간 성능을 제공한다고 설명합니다.
함께 보면 좋은 토픽
참고 자료
Java — O(1) 상수 시간
/* ── 입력 크기가 아무리 커져도 항상 딱 한 번만 연산 ────────── */ public int getFirst(int[] arr) { return arr[0]; // 배열 길이와 무관하게 즉시 반환 }
배열이 10개짜리든 1,000,000개짜리든, 인덱스로 값 하나를 꺼내는 데 걸리는 연산 횟수는 항상 1번입니다.
입력 크기 n과 실행 시간이 아예 무관하기 때문에 O(1)이라고 표기합니다.
Java — O(n) 선형 시간
/* ── 배열을 한 번 훑으며 최댓값을 찾음 ─────────────────────── */ public int findMax(int[] arr) { int max = arr[0]; for (int i = 1; i < arr.length; i++) { // arr.length번 반복 if (arr[i] > max) max = arr[i]; } return max; }
반복문이 arr.length번, 즉 입력 크기 n만큼 정확히 한 번씩 실행됩니다.
n이 2배로 늘면 연산 횟수도 2배로 늘어나는, 입력 크기에 정비례하는 O(n) 시간복잡도입니다.
Java — O(n²) 이차 시간
/* ── 이중 for문: 모든 쌍(i, j)을 서로 비교 ─────────────────── */ public boolean hasDuplicate(int[] arr) { for (int i = 0; i < arr.length; i++) { for (int j = i + 1; j < arr.length; j++) { // i마다 arr.length번 가까이 반복 if (arr[i] == arr[j]) return true; } } return false; }
바깥 반복문이 n번 돌 때마다 안쪽 반복문도 매번 n번 가까이 돌기 때문에, 전체 연산 횟수는 대략 n × n번입니다.
n이 2배로 늘면 연산 횟수는 4배로 늘어나는 O(n²) 시간복잡도이며, ds-09(정렬 알고리즘)의 버블 정렬이 대표적인 예입니다.
O(log n)은 다음 토픽(이진 탐색)에서, O(2ⁿ)은 ds-10(동적 프로그래밍)의 완전 탐색에서 이미 확인했습니다.
Q1
입력 크기가 커질수록 O(1)과 O(n) 중 더 느리게 증가하는 것은?
Q2
O(n^2)과 O(n log n) 중 큰 n에서 더 느린 것은?
Q3
Big-O 표기법이 나타내는 것은?