시간 복잡도 이해하기
by yoora · 2026-02-04
시간 복잡도란?
시간 복잡도는 입력 크기(n)가 커질수록 알고리즘이 수행하는 연산량이 어떤 성장률로 증가하는지를 나타낸다.
실제 실행시간(ms)이 아니라, 입력 증가에 따른 "성장 추세"로 알고리즘 효율을 비교한다.
왜 실제 시간(ms) 대신 수학적으로 표현할까?
- 같은 코드라도 하드웨어/OS/런타임/백그라운드 작업 등에 따라 실제 시간은 달라져 비교가 어렵다.
- 따라서, 입력 크기 n에 따른 성장률로 추상화하여 점근적 표기로 비교한다.
즉, 시간 복잡도는 "얼마나 오래걸리는지"가 아니라 "입력이 커질 때 얼마나 빨리 느려지는지"를 보는 지표이다.
시간 복잡도를 표기하는 방법: Big-O 표기법
1. 빅오(Big-O) 표기법이란
빅오(Big-O) 표기법은 알고리즘의 실행 시간이 입력 크기 n에 따라 어느 정도로 증가하는지(성장률)를 표현하는 방식이다.
보통 최악의 경우(Worst Case)를 기준으로 "아무리 운이 나빠도 이 정도 시간 안에는 끝난다"는 상한선(Upper Bound)를 나타낸다.
2. Big-O 표기법의 수학적 규칙
1) 가장 큰 항만 남긴다.
입력 n이 충분히 커질수록, 가장 빨리 커지는 항이 전체 성능을 지배한다.
예: f(n) = 3n² + 10n + 500
- n이 커지면 n² 항이 압도적으로 커짐
-> 따라서 O(n²)로 표현
2) 상수는 무시한다 (증가 "비율"만 중요)
성장률을 볼 떄는 상수 배수나 상수 시간은 복잡도에 큰 영향을 주지 않으므로 제거한다.
- O(5n) -> O(n)
- O(1000) -> O(1)
3. 자주 쓰는 Big-O 종류
1) O(1) 상수 시간
- 입력 크기 n과 상관없이 항상 일정한 시간
- 예: 배열 특정 인덱스 접근 arr[5]
2) O(log n) 로그 시간
- 데이터가 커져도 시간이 조금씩만 증가
- 예: 이진 탐색(범위를 절반씩 줄이는 Up/Down 방식)
3) O(n) 선형 시간
- 입력 크기 n에 비례해서 실행 시간이 증가
- 예: for로 배열 전체를 한 번 순회
4) O(n²) 제곱 시간
- 입력이 늘어나면 시간이 제곱으로 급격히 증가
- 예: 이중 for문(구구단처럼 중첩 반복)
4. 실행속도를 숫자로 확인해보기
입력 크기 n = 1,000 일 때,
| 시간 복잡도 | 연산 횟수 |
|---|---|
| O(1) | 1 |
| O(log n) | 약 10 |
| O(n) | 1,000 |
| O(n log n) | 약 10,000 |
| O(n²) | 1,000,000 |
시간 복잡도는 버그가 아니라 구조적인 성능 문제이므로, 프로그래밍에서 중요하다.
JavaScript 코드로 시간 복잡도 이해하기
1. 단순하지만 가장 중요한 기준: "몇 번 도는가?"
- O(1): 한 번에 접근 (상수 시간)
JavaScriptJavaScriptfunction accessArray(arr) { return arr[2]; }- 배열 길이와 상관없이 항상 한 번 접근
-> 따라서 O(1)
- 배열 길이와 상관없이 항상 한 번 접근
- O(n): 한 번 돌기 (선형 시간)
JavaScriptJavaScriptfunction sum(arr){ let total = 0; for (let i = 0; i < arr.length; i++){ total += arr[i]; } return total; }- 배열 길이가 n일 때 반복문이 n번
-> 따라서 O(n)
- 배열 길이가 n일 때 반복문이 n번
- O(n²): 중첩 반복 (제곱 시간)
JavaScriptJavaScriptfunction countPairs(arr) { let count = 0; for(let i = 0; i < arr.length; i++){ for(let j = 0; j < arr.length; j++){ count++; } } return count; }- 바깥 루프 n번 x 안쪽 루프 n번
-> 따라서 O(n²) - 입력이 10배면 실행량은 100배가 되기 쉽다.
- 바깥 루프 n번 x 안쪽 루프 n번
2. 프론트엔드에서 "O(n²)"가 자주 생기는 경우
"배열을 한 번 돌면서, 그 안에서 또 배열을 탐색하는 방식"
특히 includes, find, filter 같은 메서드는 내부적으로 앞에서부터 순차 탐색(선형 탐색) 형태가 되기 쉬워서, 중첩되면 O(n²)이 된다.
- 문제가 될 수 있는 중첩 코드 #1 : map + includes 조합💡 느려지는 이유
JavaScriptJavaScriptfunction selectComponent(items, selectedIds) { return items.map(item => ({ ...item, selected: selectedIds.includes(item.id) })); }- items.map: n번 반복
- selectedIds.includes(...): 매번 m개를 선형 탐색
- 결과적으로 O(nm)
- 보통 n≈m이면 체감상 O(n²)
개선 포인트JavaScriptJavaScriptfunction selectComponent(items, selectedIds) { const selectedSet = new Set(selectedIds) return items.map(item => ({ ...item, selected: selectedSet.has(item.id) })); }- Set.has()는 평균적으로 빠른 체크가 가능하도록 설계된다.
- 전처리 new Set(selectedIds) : O(m)
- 전체 map 검사 : O(n)
- 총합 O(n + m)로 개선
- 문제가 될 수 있는 중첩 코드 #2 : some + includes 조합💡 느려지는 이유
JavaScriptJavaScript// 두 배열에 공통 값이 있는지 확인 function hasAnyCommon(a, b) { return a.some(x => b.includes(x)); }- some: 최악 n번
- includes: 최악 m번
- 최악 O(nm)
JavaScriptJavaScriptfunction hasAnyCommon(a, b) { const setB = new Set(b); return a.some(x => setB.has(x)); }- 전처리 O(m) + 검사 O(n)
-> O(n + m)
- 문제가 될 수 있는 중첩 코드 #3 : filter 중첩💡 느려지는 이유
JavaScriptJavaScript// ex. 카테고리 목록 : products: n개, categories: m개 const ui = categories.map(cat => ({ ...cat, items: products.filter(p => p.categoryId === cate.id) }));- categories.map: m번
- 내부 products.filter: 매번 n번
- 총합 O(nm)
JavaScriptJavaScriptfunction groupByCategory(products){ const bucket = new Map(); for(const p of products){ if(!bucket.has(p.categoryId)) bucket.set(p.categoryId, []); bucket.get(p.categoryId).push(p); } return bucket; } const bucket = groupByCategory(products); const ui = categories.map(cat => ({ ...cat, items: bucket.get(cat.id) ?? [] }));- 그룹핑: O(n)
- UI 구성: O(m)
- 총합: O(n + m)
- 문제가 될 수 있는 중첩 코드 #4 : 루프 안에서 매번 querySelector 호출💡 느려지는 이유
JavaScriptJavaScriptfunction updateBad(rows){ for (const row of rows) { const el = document.querySelector(`[data-id="${row.id}"]`); if(el) el.textContent = row.value; } }- querySelector 자체가 DOM 트리에서 탐색 비용이 있고, DOM이 큰 페이지일수록 탐색/스타일/레이아웃 비용이 커져서 반복시 체감 성능이 급격히 무너질 수 있기 때문이다.
JavaScriptJavaScriptfunction updateBetter(rows){ const map = new Map(); document.querySelectorAll("[data-id]").forEach(el => { map.set(el.dataset.id, el); }); for(const row of rows){ const el = map.get(String(row.id)); if(el) el.textContent = row.value; } }- "DOM 탐색"을 반복문 안에서 하지 않고 한번만 수집
-> "빠른 조회 구조(Map)"로 변경