프로그램의 효율성을 판단하는 기준인 시간복잡도를 다룹니다.
같은 결과를 만드는 코드라도 데이터가 많아지면 실행 시간에 큰 차이가 생길 수 있습니다.
시간복잡도를 이해하면 단순히 정답을 만드는 것을 넘어, 입력 크기에 맞는 풀이를 선택할 수 있습니다.
1. 시간복잡도란?
시간복잡도는 입력 데이터의 크기가 증가할 때 연산 횟수가 얼마나 증가하는지를 나타냅니다.
실제 실행 시간을 초 단위로 계산하는 것이 아니라, 입력 크기에 따른 연산량의 증가 정도를 표현합니다.
입력 크기는 일반적으로 N으로 나타냅니다.
numbers = [1, 2, 3, 4, 5]
위 리스트의 길이가 N이라면, 모든 값을 한 번씩 확인하는 코드는 약 N번의 연산을 수행합니다.
for number in numbers:
print(number)
- 이 코드의 시간복잡도는 O(N)입니다.
2. Big-O 표기법
시간복잡도는 주로 Big-O 표기법으로 나타냅니다.
Big-O는 입력이 충분히 커졌을 때 연산량이 어떤 형태로 증가하는지 표현합니다.
대표적인 시간복잡도는 다음과 같습니다.
| 시간복잡도 | 의미 | 예시 |
|---|---|---|
| O(1) | 입력 크기와 관계없이 일정 | 인덱스로 리스트 값 조회 |
| O(log N) | 탐색 범위를 반복해서 줄임 | 이분 탐색 |
| O(N) | 데이터를 한 번 순회 | 최댓값 찾기 |
| O(N log N) | 효율적인 정렬에서 자주 등장 | sort(), sorted() |
| O(N²) | 모든 데이터 쌍을 확인 | 이중 반복문 |
| O(2ⁿ) | 선택에 따라 경우의 수가 두 배씩 증가 | 부분집합 완전탐색 |
일반적으로 아래로 갈수록 입력 크기가 커졌을 때 실행 시간이 빠르게 증가합니다.
O(1) < O(log N) < O(N) < O(N log N) < O(N²) < O(2ⁿ)
3. O(1)
입력 크기와 관계없이 연산 횟수가 일정한 경우입니다.
numbers = [10, 20, 30, 40]
print(numbers[0])
- 리스트의 길이가 4이든 1,000,000이든 첫 번째 값에 접근하는 연산 횟수는 크게 달라지지 않습니다.
딕셔너리와 집합의 조회도 평균적으로 O(1)입니다.
scores = {"Alice": 90, "Bob": 80}
print(scores["Alice"])
4. O(N)
입력 데이터를 한 번 순회하는 경우입니다.
numbers = [3, 8, 2, 7, 5]
maximum = numbers[0]
for number in numbers:
if (number > maximum):
maximum = number
print(maximum)
- 데이터가 N개라면 반복문도 약 N번 실행되므로 O(N)입니다.
연속된 반복문이 여러 개 있어도 O(N)일 수 있습니다.
for number in numbers:
print(number)
for number in numbers:
print(number * 2)
- 전체 연산량은 약 2N이지만 Big-O에서는 증가 형태에 집중하므로 상수 2를 제외하고 O(N)으로 표현합니다.
5. O(N²)
중첩 반복문에서 각 반복문이 N번 실행되면 O(N²)이 됩니다.
numbers = [1, 2, 3, 4]
for a in numbers:
for b in numbers:
print(a, b)
- 데이터가 N개일 때 가능한 모든 쌍을 확인하므로 약 N × N번 실행됩니다.
하지만 중첩 반복문이라고 해서 항상 O(N²)인 것은 아닙니다.
for i in range(N):
for j in range(10):
print(i, j)
- 안쪽 반복문은 항상 10번만 실행되므로 전체 연산량은 10N입니다.
- 상수를 제외하면 시간복잡도는 O(N)입니다.
6. 리스트와 집합의 차이
특정 값이 존재하는지 확인하는 코드를 비교해보겠습니다.
numbers = [1, 2, 3, 4, 5]
print(5 in numbers)
리스트는 앞에서부터 값을 확인할 수 있으므로 존재 여부 확인에 O(N)이 필요합니다.
numbers = {1, 2, 3, 4, 5}
print(5 in numbers)
- 집합은 평균적으로 O(1)에 존재 여부를 확인할 수 있습니다.
여러 값을 반복해서 검색해야 한다면 자료구조의 차이가 커집니다.
registered = [101, 102, 103, 104]
queries = [102, 105, 103]
for member_id in queries:
if member_id in registered:
print(member_id)
- registered와 queries의 길이가 모두 N이라면 리스트에서는 최대 O(N²)이 필요할 수 있습니다.
registered를 집합으로 바꾸면 각 조회가 평균 O(1)이므로 전체 조회는 평균 O(N)이 됩니다.
registered = {101, 102, 103, 104}
for member_id in queries:
if member_id in registered:
print(member_id)
7. 주요 Python 연산의 시간복잡도
리스트
| 연산 | 평균 시간복잡도 |
|---|---|
list[i] |
O(1) |
append() |
O(1) |
pop() |
O(1) |
insert(0, value) |
O(N) |
pop(0) |
O(N) |
value in list |
O(N) |
sort() |
O(N log N) |
- 리스트의 앞에 값을 추가하거나 제거하면 나머지 값을 이동해야 하므로 O(N)이 필요합니다.
딕셔너리와 집합
| 연산 | 평균 시간복잡도 |
|---|---|
| key 또는 값 조회 | O(1) |
| 값 추가 | O(1) |
| 값 삭제 | O(1) |
value in set |
O(1) |
- 딕셔너리와 집합의 연산은 평균적으로 O(1)이지만, 충돌이 많이 발생하는 최악의 경우에는 더 느려질 수 있습니다.
deque
| 연산 | 시간복잡도 |
|---|---|
append() |
O(1) |
appendleft() |
O(1) |
pop() |
O(1) |
popleft() |
O(1) |
- 앞과 뒤에서 값을 자주 추가하거나 제거한다면 리스트보다 deque가 적합합니다.
8. 입력 크기로 풀이 예상하기
문제를 풀기 전에 입력 제한을 확인하면 사용할 수 있는 풀이를 예상할 수 있습니다.
정확한 기준은 언어와 문제 환경에 따라 달라지지만, 일반적으로 다음과 같이 생각할 수 있습니다.
| 입력 크기 | 고려할 수 있는 시간복잡도 |
|---|---|
| N ≤ 20 | O(2ⁿ), 완전탐색 |
| N ≤ 500 | O(N³), O(N²) |
| N ≤ 10,000 | O(N²)은 주의 |
| N ≤ 100,000 | O(N log N), O(N) |
| N ≤ 1,000,000 | O(N), O(log N) |
- 예를 들어 N = 100,000일 때 O(N²) 풀이는 최대 약 100억 번의 연산이 필요합니다.
- 반면 O(N log N) 또는 O(N) 풀이는 훨씬 현실적입니다.
9. 공간복잡도
공간복잡도는 프로그램이 사용하는 메모리의 양을 나타냅니다.
N = 100
visited = [False] * N
- 입력 크기 N에 비례하는 리스트를 만들었으므로 추가 공간은 O(N)입니다.
total = 0
for number in range(N):
total += number
- 입력 크기가 커져도 몇 개의 변수만 사용하므로 추가 공간은 O(1)입니다.
실행 시간을 줄이기 위해 집합이나 딕셔너리를 추가로 사용하는 경우가 많습니다.
이처럼 시간과 메모리 사이에는 선택이 필요할 수 있습니다.
10. 종합 예제
리스트에서 중복된 값을 찾는 두 가지 방법을 비교해보겠습니다.
모든 값의 쌍 비교
def has_duplicate(numbers):
for i in range(len(numbers)):
for j in range(i + 1, len(numbers)):
if (numbers[i] == numbers[j]):
return True
return False
- 최악의 경우 모든 값의 쌍을 확인하므로 시간복잡도는 O(N²)입니다.
집합 활용
def has_duplicate(numbers):
seen = set()
for number in numbers:
if number in seen:
return True
seen.add(number)
return False
- 집합의 조회와 추가는 평균 O(1)이므로 전체 시간복잡도는 평균 O(N)입니다.
- 대신 최대 N개의 값을 집합에 저장하므로 공간복잡도는 O(N)입니다.
11. 자주 하는 실수/주의점
- 실제 실행 시간과 시간복잡도는 같은 의미가 아닙니다.
- 반복문이 두 개 있다고 무조건
O(N²)인 것은 아닙니다. - 중첩 반복문의 각 반복 횟수를 확인해야 합니다.
- Big-O에서는 보통 상수와 영향력이 작은 항을 제외합니다.
- 딕셔너리와 집합 연산의
O(1)은 평균적인 경우입니다. - 실행 시간을 줄이기 위해 추가 메모리를 사용할 수 있습니다.
- 문제의 입력 제한을 확인하지 않고 풀이 방법부터 정하지 않도록 주의해야 합니다.
12. 종합 요약
- 시간복잡도: 입력 크기에 따른 연산량의 증가 정도
- O(1): 입력 크기와 관계없이 일정
- O(N): 전체 데이터를 한 번 순회
- O(N log N): 효율적인 정렬에서 자주 등장
- O(N²): 모든 데이터 쌍을 확인할 때 자주 등장
- 리스트 조회는 위치 접근과 값 검색의 복잡도가 다름
- 집합과 딕셔너리는 평균적으로 빠른 검색이 가능
- 입력 제한을 확인하고 적절한 알고리즘과 자료구조를 선택
- 공간복잡도까지 함께 고려하면 더 효율적인 코드를 작성할 수 있음
'GDGoC > Python' 카테고리의 다른 글
| [Python] Algorithm #06: BFS와 DFS (0) | 2026.08.12 |
|---|---|
| [Python] Algorithm #05: 동적 계획법 (Dynamic Programming) (0) | 2026.08.12 |
| [Python] Basic #06: 자료구조 (0) | 2026.08.12 |
| [Python] Basic #05: 반복문 심화 (0) | 2026.08.12 |
| [Python] Algorithm #04: 완전탐색과 백트래킹 (0) | 2026.05.11 |