시간복잡도란? Big-O 표기법과 코드 분석으로 이해하기
1. 시간복잡도란?
입력 크기 N이 커질수록 알고리즘이 얼마나 느려지는지를 예측하는 도구
시간복잡도는 입력 크기가 증가할 때 알고리즘의 실행 시간이 얼마나 빠르게 증가하는지를 나타내는 개념입니다.
여기서 중요한 점은 실제 실행 시간이 몇 초인지가 아니라, 입력 데이터의 크기 N이 커질수록 연산 횟수가 어떤 비율로 증가하는지를 보는 것입니다.
시간복잡도를 이해해야 하는 이유는 단순합니다.
작은 입력에서는 잘 동작하는 코드도 큰 입력에서는 전혀 사용할 수 없을 수 있기 때문입니다. 코딩 테스트나 실무 시스템에서는 데이터가 수천 개, 수백만 개 이상으로 커질 수 있습니다.
🔷 Big-O 복잡도 그래프

2. Big-O 표기법 이해하기
시간복잡도를 표현할 때 가장 많이 사용하는 방식이 Big-O 표기법입니다.
Big-O 표기법은 알고리즘의 실행 시간이 입력 크기 N에 따라 얼마나 증가하는지를 간단하게 나타냅니다.
✔️ 가장 대표적인 시간복잡도는 다음과 같습니다.
| 표기법 | 의미 | 예시 |
| O(1) | 입력 크기와 관계없이 일정한 시간 | 배열 인덱스 접근 |
| O(log N) | 입력이 커져도 실행 횟수가 천천히 증가 | 이진 탐색 |
| O(N) | 입력 크기에 비례해서 증가 | 단일 반복문 |
| O(N log N) | 효율적인 정렬 알고리즘에서 자주 등장 | 병합 정렬, 퀵 정렬 평균 |
| O(N²) | 입력 크기의 제곱만큼 증가 | 중첩 반복문 |
✔️ 예를 들어 배열에서 특정 인덱스의 값을 가져오는 작업은 입력 크기와 관계없이 한 번에 접근할 수 있습니다.
int value = arr[3];
이 경우 배열의 크기가 10개든 100만 개든 특정 위치에 접근하는 시간은 거의 일정합니다. 따라서 시간복잡도는 O(1)입니다.
✔️ 반면, 배열에서 특정 값을 찾기 위해 처음부터 끝까지 확인해야 한다면 입력 크기만큼 반복해야 합니다.
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return i;
}
}
이 경우 최악의 상황에서는 배열의 모든 요소를 확인해야 하므로 시간복잡도는 O(N)입니다.
✔️ 또한 반복문 안에 반복문이 있는 경우에는 실행 횟수가 크게 증가합니다.
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(i + ", " + j);
}
}
위 코드는 바깥 반복문이 n번 실행되고, 그때마다 안쪽 반복문도 n번 실행됩니다. 따라서 총 실행 횟수는 n × n이 되어 시간복잡도는 O(N²)입니다.
3. 코드 예제로 이해하는 시간복잡도 분석
시간복잡도를 가장 잘 이해하는 방법은 실제 코드를 보면서 분석해 보는 것입니다.
코드에서 반복문이 몇 번 실행되는지, 반복문이 서로 중첩되어 있는지, 또는 반복이 진행될수록 입력 크기가 줄어드는 구조인지에 따라 시간복잡도는 달라집니다.
✔️ 한 번만 실행되는 코드: O(1)
int a = 10;
int b = 20;
int sum = a + b;
System.out.println(sum);
int value = arr[3];
위 코드는 입력 크기와 관계없이 정해진 횟수만 실행됩니다.
데이터가 10개이든 10만 개이든 실행 횟수는 거의 변하지 않습니다.
따라서 시간복잡도는 O(1)입니다.
이처럼 입력 크기와 무관하게 일정한 시간이 걸리는 코드를 상수 시간이라고 합니다.
✔️ 단일 반복문: O(N)
for (int i = 0; i < n; i++) {
System.out.println(i);
}
위 코드는 i가 0부터 n - 1까지 증가하므로 총 n번 실행됩니다.
즉, 입력 크기 n이 10이면 10번, 1,000이면 1,000번 실행됩니다.
따라서 시간복잡도는 O(N)입니다.
배열 전체를 순회하면서 합계를 구하는 코드도 같은 방식으로 분석할 수 있습니다.
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
배열의 모든 요소를 한 번씩 확인하므로 배열의 길이를 N이라고 할 때 시간복잡도는 O(N)입니다.
✔️ 조건문이 포함된 반복문: O(N)
for (int i = 0; i < n; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
위 코드에는 반복문 안에 조건문이 들어 있습니다.
하지만 조건문이 포함되어 있다고 해서 시간복잡도가 크게 달라지는 것은 아닙니다.
반복문은 여전히 n번 실행되고, 각 반복에서 짝수인지 확인하는 연산은 상수 시간에 처리됩니다.
따라서 전체 시간복잡도는 O(N)입니다.
즉, 반복문 내부에 간단한 조건문이나 계산식이 들어가더라도, 전체 반복 횟수가 N번이라면 보통 O(N)으로 분석합니다.
✔️ 두 개의 반복문이 나란히 있는 경우: O(N)
for (int i = 0; i < n; i++) {
System.out.println(i);
}
for (int j = 0; j < n; j++) {
System.out.println(j);
}
첫 번째 반복문은 n번 실행되고, 두 번째 반복문도 n번 실행됩니다.
따라서 전체 실행 횟수는 다음과 같습니다.
n + n = 2N
처음에는 시간복잡도가 O(2N)처럼 보일 수 있습니다.
하지만 Big-O 표기법에서는 상수 계수를 제거합니다.
따라서 시간복잡도는 O(N)입니다.
반복문이 세 개여도 마찬가지입니다.
for (int i = 0; i < n; i++) {
System.out.println(i);
}
for (int j = 0; j < n; j++) {
System.out.println(j);
}
for (int k = 0; k < n; k++) {
System.out.println(k);
}
각 반복문이 독립적으로 n번씩 실행되므로 전체 실행 횟수는 3N입니다.
하지만 Big-O 표기법에서는 상수 계수를 제외하므로 이 경우에도 시간복잡도는 O(N)입니다.
✔️ 서로 다른 입력 크기를 사용하는 반복문: O(N + M)
for (int i = 0; i < n; i++) {
System.out.println(i);
}
for (int j = 0; j < m; j++) {
System.out.println(j);
}
첫 번째 반복문은 n번 실행되고, 두 번째 반복문은 m번 실행됩니다.
이때 n과 m은 서로 다른 입력 크기입니다.
따라서 전체 시간복잡도는 O(N + M)으로 표현합니다.
만약 n과 m이 항상 비슷한 규모라고 가정할 수 있다면 O(N)처럼 단순화해서 볼 수도 있습니다.
하지만 두 입력 크기가 서로 독립적이라면 O(N + M)으로 표현하는 것이 더 정확합니다.
✔️ 중첩 반복문: O(N²)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(i + ", " + j);
}
}
바깥 반복문은 n번 실행됩니다.
그리고 바깥 반복문이 한 번 실행될 때마다 안쪽 반복문도 n번 실행됩니다.
따라서 전체 실행 횟수는 다음과 같습니다
n × n = n²
이 경우 시간복잡도는 O(N²)입니다.
중첩 반복문은 입력 크기가 커질수록 실행 횟수가 매우 빠르게 증가합니다.
예를 들어 n = 1,000이면 약 1,000,000번의 반복이 발생합니다.
그래서 중첩 반복문을 사용할 때는 입력 크기가 커졌을 때 실행 시간이 얼마나 늘어나는지 반드시 고려해야 합니다.
✔️ 삼중 반복문: O(N³)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
System.out.println(i + ", " + j + ", " + k);
}
}
}
이 코드는 반복문이 세 단계로 중첩되어 있습니다.
각 반복문이 모두 n번씩 실행되므로 전체 실행 횟수는 다음과 같습니다.
n × n × n = n³
따라서 시간복잡도는 O(N³)입니다.
이런 구조는 입력 크기가 조금만 커져도 실행 시간이 크게 증가합니다.
따라서 실무나 코딩 테스트에서는 꼭 필요한 경우가 아니라면 피하는 것이 좋습니다.
✔️ 안쪽 반복문의 범위가 바깥 변수에 따라 달라지는 경우: O(N²)
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
System.out.println(i + ", " + j);
}
}
이 코드는 안쪽 반복문이 항상 n번 실행되지는 않습니다.
i = 0일 때는 0번,
i = 1일 때는 1번,
i = 2일 때는 2번,
마지막에는 n - 1번 실행됩니다.
전체 실행 횟수는 다음과 같습니다.
0 + 1 + 2 + ... + (n - 1)
이 합은 대략 다음과 같습니다.
n(n - 1) / 2
Big-O 표기법에서는 상수와 낮은 차수를 제거하므로 최종 시간복잡도는 O(N²)입니다.
중요한 점은 정확한 실행 횟수가 n² / 2에 가깝더라도, 증가율 기준으로는 O(N²)으로 본다는 것입니다.
✔️ 반복 횟수가 절반씩 줄어드는 경우: O(log N)
while (n > 1) {
n = n / 2;
}
위 코드는 반복이 한 번 실행될 때마다 n이 절반으로 줄어듭니다.
예를 들어 n = 16이라면 다음과 같이 변합니다.
16 → 8 → 4 → 2 → 1
총 4번 정도 반복됩니다.
n = 1,024라면 약 10번 반복됩니다.
즉, 입력 크기가 커지더라도 반복 횟수는 매우 천천히 증가합니다.
이처럼 입력 크기가 절반씩 줄어드는 구조의 시간복잡도는 보통 O(log N)입니다.
✔️ 반복문 안에서 절반씩 줄어드는 작업: O(N log N)
for (int i = 0; i < n; i++) {
int temp = n;
while (temp > 1) {
temp = temp / 2;
}
}
이 코드는 바깥 반복문과 안쪽 while문을 함께 봐야 합니다.
바깥 반복문은 n번 실행됩니다.
그리고 안쪽 while문은 매번 log N번 정도 실행됩니다.
따라서 전체 시간복잡도는 다음과 같습니다.
N × log N
즉, 시간복잡도는 O(N log N)입니다.
이 복잡도는 효율적인 정렬 알고리즘에서 자주 등장합니다.
대표적으로 병합 정렬과 힙 정렬이 있습니다.
✔️ 배열에서 특정 값을 찾는 경우: O(N)
int findIndex(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
이 코드는 배열의 앞에서부터 값을 하나씩 확인합니다.
운이 좋으면 첫 번째 요소에서 바로 값을 찾을 수 있습니다.
이 경우는 최선의 경우이며, 시간복잡도는 O(1)입니다.
하지만 찾는 값이 배열의 마지막에 있거나, 아예 배열 안에 없다면 모든 요소를 확인해야 합니다.
이 경우는 최악의 경우이며, 시간복잡도는 O(N)입니다.
일반적으로 시간복잡도를 말할 때는 최악의 경우를 기준으로 설명하는 경우가 많습니다.
따라서 이 코드는 보통 O(N)으로 분석합니다.
✔️ HashMap을 사용한 조회: 평균 O(1)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 100);
map.put("banana", 200);
int price = map.get("apple");
HashMap은 키를 이용해서 값을 빠르게 조회할 수 있는 자료구조입니다.
일반적으로 HashMap의 삽입, 조회, 삭제 연산은 평균적으로 O(1)에 가깝습니다.
배열이나 리스트에서 특정 값을 찾으려면 앞에서부터 하나씩 비교해야 하므로 O(N)이 걸릴 수 있습니다.
하지만 HashMap은 키를 기준으로 값을 바로 찾을 수 있기 때문에 훨씬 효율적으로 동작합니다.
✔️ 리스트에서 삭제할 때의 시간복잡도
자료구조에 따라 같은 작업이라도 시간복잡도가 달라질 수 있습니다.
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
list.remove(0);
ArrayList에서 맨 앞 요소를 삭제하면 뒤에 있는 요소들을 앞으로 한 칸씩 이동해야 합니다.
따라서 삭제 위치에 따라 시간복잡도는 O(N)이 될 수 있습니다.
반면 마지막 요소를 삭제하는 경우는 상대적으로 빠릅니다.
list.remove(list.size() - 1);
마지막 요소는 뒤에 이동시킬 요소가 없으므로 보통 O(1)에 가깝습니다.
이처럼 같은 삭제 연산이라도 위치와 자료구조에 따라 성능이 달라집니다.
4. 시간복잡도 개선 사례
시간복잡도는 단순히 코드를 분석하기 위한 개념이 아닙니다.
더 효율적인 알고리즘이나 자료구조를 선택하기 위한 기준이기도 합니다.
✔️ 배열 검색: O(N)에서 O(1)로 개선하기
먼저 배열에서 특정 값이 있는지 확인하는 코드입니다.
boolean containsValue(int[] arr, int target) {
for (int num : arr) {
if (num == target) {
return true;
}
}
return false;
}
이 코드는 배열을 처음부터 끝까지 순회하면서 target이 있는지 확인합니다.
최악의 경우 배열의 모든 요소를 확인해야 하므로 시간복잡도는 O(N)입니다.
이 작업이 한 번만 필요하다면 큰 문제가 없을 수 있습니다.
하지만 같은 배열에서 여러 번 값을 찾아야 한다면 매번 O(N)이 소요되어 비효율적입니다.
이 경우 HashSet을 사용하면 조회 성능을 개선할 수 있습니다.
Set<Integer> set = new HashSet<>();
for (int num : arr) {
set.add(num);
}
boolean result = set.contains(target);
HashSet에 값을 미리 저장해 두면 contains 연산은 평균적으로 O(1)입니다.
따라서 여러 번 조회해야 하는 상황에서는 배열을 매번 순회하는 것보다 훨씬 효율적입니다.
단, HashSet을 만드는 데는 한 번 O(N)의 시간이 필요합니다. 따라서 조회가 여러 번 반복되는 상황에서 특히 효과적인 개선 방법입니다.
✔️ 두 수의 합 찾기: O(N²)에서 O(N)으로 개선하기
먼저 모든 숫자 쌍을 비교하는 방식입니다.
boolean hasTwoSum(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] + arr[j] == target) {
return true;
}
}
}
return false;
}
이 코드는 모든 숫자 쌍을 하나씩 확인합니다.
바깥 반복문과 안쪽 반복문이 중첩되어 있으므로 최악의 경우 시간복잡도는 O(N²)입니다.
HashSet을 사용하면 한 번의 순회로 개선할 수 있습니다.
boolean hasTwoSum(int[] arr, int target) {
Set<Integer> set = new HashSet<>();
for (int num : arr) {
int needed = target - num;
if (set.contains(needed)) {
return true;
}
set.add(num);
}
return false;
}
현재 숫자 num에 대해 필요한 값은 target - num입니다.
이미 그 값이 HashSet에 존재한다면 두 수의 합으로 target을 만들 수 있습니다.
배열을 한 번만 순회하고, HashSet의 조회와 삽입은 평균적으로 O(1)이므로 전체 시간복잡도는 O(N)입니다.
✔️ 문자열 누적: O(N²)에서 O(N)으로 개선
먼저 비효율적인 문자열 누적 방식입니다.
String result = "";
for (int i = 0; i < n; i++) {
result += i;
}
Java의 String은 불변 객체입니다.
따라서 문자열을 더할 때마다 새로운 문자열이 생성됩니다.
반복이 진행될수록 기존 문자열을 복사하는 비용이 커지기 때문에 전체 시간복잡도는 O(N²)에 가까워질 수 있습니다.
이 경우 StringBuilder를 사용하는 것이 좋습니다.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(i);
}
String result = sb.toString();
StringBuilder는 내부 버퍼를 사용하여 문자열을 효율적으로 추가합니다.
따라서 반복문 안에서 문자열을 누적해야 할 때는 String 덧셈보다 훨씬 적합합니다.
전체 시간복잡도는 일반적으로 O(N)에 가깝게 개선됩니다.
✔️ 리스트 반복 검색: O(N)에서 O(1) 조회로 개선하기
먼저 List에서 특정 값이 존재하는지 확인하는 코드입니다.
boolean exists(List<String> names, String target) {
for (String name : names) {
if (name.equals(target)) {
return true;
}
}
return false;
}
List에서 특정 값이 존재하는지 확인하려면 앞에서부터 하나씩 비교해야 합니다.
따라서 한 번의 검색은 최악의 경우 O(N)입니다.
검색이 한 번이라면 괜찮을 수 있지만, 같은 목록에서 여러 번 검색한다면 성능이 나빠질 수 있습니다.
이 경우 HashSet으로 변환해 두면 검색 비용을 줄일 수 있습니다.
Set<String> nameSet = new HashSet<>(names);
boolean result = nameSet.contains(target);
HashSet의 contains 연산은 평균적으로 O(1)입니다.
처음 HashSet을 만드는 데 O(N)이 필요하지만, 이후 여러 번 검색할 때는 훨씬 효율적입니다.
즉, 반복 조회가 많은 상황에서는 List보다 HashSet이 더 적합할 수 있습니다.
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'5. IT기술노트 > 기타' 카테고리의 다른 글
| 스케어웨어(Scareware)란 무엇인가?: 공포를 무기로 삼는 사이버 공격 (0) | 2025.12.26 |
|---|---|
| 원더랜드(Wonderland) 악성코드 이해하기: 구글 플레이 앱은 정말 안전할까? (0) | 2025.12.26 |
| 순공학,역공학,리팩터링,재엔지니어링,전면 재개발까지 한 번에 정리 (0) | 2025.12.10 |
| 패스키(Passkey)의 원리와 활용: 비밀번호 없는 인증 이해하기 (0) | 2025.11.30 |
| 업무프로세스혁신 BPR·PI 쉽게 이해하기 (0) | 2025.11.24 |