[Python] 알고리즘 설계 기초
동빈나 채널의 파이썬 문법 부수기 유튜브 강의를 참고하여 정리한 내용이다.
코딩테스트를 준비하면서 이코테(이것이 취업을 위한 코딩 테스트다) 강의를 들으며 정리한 내용이다.
본격적인 자료구조나 알고리즘 문법 공부에 앞서 먼저 알아야 할 알고리즘 설계 기초 내용을 정리했다.
알고리즘 설계 Tip
시간복잡도와 수행시간 감각
코딩테스트에서 가장 먼저 봐야 할 건 시간 제한이다.
보통 1~2초가 주어지는데, 이걸 그냥 넘겨버리면 아무리 정확한 코드를 짜도 의미가 없다.
일반적으로 코딩테스트 채점 서버 기준으로 1초에 수행 가능한 연산 횟수는 대략 1억 번(10^8) 정도라고 보면 된다.
파이썬은 C/C++보다 느려서 같은 코드라도 파이썬 기준으로 더 넉넉하게 잡아야 한다.
| 언어 | 1초 기준 연산 횟수 (대략) |
|---|---|
| C / C++ | 10^8 ~ 10^9 |
| Java | 10^8 |
| Python | 10^7 ~ 10^8 |
파이썬으로 코딩테스트를 볼 때 시간 초과가 자주 뜨는 이유가 이 때문이다.
인터프리터 언어 특성상 같은 알고리즘이라도 C보다 5~10배 정도 느리다.
N 범위로 시간복잡도 추측하기
문제를 받으면 N의 범위를 보고 어떤 시간복잡도 알고리즘을 써야 하는지 역추산할 수 있다.
| N의 범위 | 가능한 시간복잡도 | 알고리즘 예시 |
|---|---|---|
| N ≤ 10 | O(N!) 이하 | 완전탐색, 순열 |
| N ≤ 20 | O(2^N) | 비트마스크, 백트래킹 |
| N ≤ 500 | O(N^3) | 플로이드-워셜 |
| N ≤ 3,000 | O(N^2) | 이중 반복문 |
| N ≤ 100,000 | O(N log N) | 정렬, 세그먼트 트리 |
| N ≤ 1,000,000 | O(N) 또는 O(N log N) | 해시, 투 포인터 |
| N ≤ 10,000,000 | O(N) | 단순 반복, 선형 탐색 |
예를 들어 N이 100만이면 O(N^2) 알고리즘은 10^12번의 연산이 필요해서 절대 통과가 안 된다.
N log N 이하로 해결해야 한다.
문제를 보자마자 N 범위 확인 → 가능한 시간복잡도 추산 → 알고리즘 선택 순서로 접근한다.
알고리즘 문제 해결 과정
이코테에서 제시하는 4단계 문제 해결 과정이다.
1단계 : 지문 읽기 및 핵심 내용 파악
문제를 천천히 읽고 무엇을 구해야 하는지 파악한다. 조건과 예외 케이스도 이 단계에서 체크한다. 급하게 코드 짜다가 엣지 케이스 놓치면 WA(오답) 뜨고 시간만 버린다.
2단계 : 요구사항에 맞는 알고리즘 설계
N 범위를 보고 어떤 알고리즘을 써야 할지 정한다. 손으로 로직을 먼저 그려보는 게 좋다. 머릿속에서만 설계하면 구현하다가 막힐 때 방향을 잃기 쉽다.
3단계 : 코드 구현
설계한 로직을 실제 코드로 옮긴다. 이 단계에서 문법 실수나 인덱스 오류가 자주 나온다. 차분하게 짜고, 주요 변수에 print를 찍어가며 확인하는 게 효율적이다.
4단계 : 테스트 및 디버깅
제공된 예제 케이스로 먼저 테스트하고, 직접 엣지 케이스(N=0, N=1, 최댓값 등)를 추가로 만들어 검증한다.
수행 시간 측정
코드가 실제로 얼마나 걸리는지 확인하고 싶을 때 time 모듈을 사용한다.
import time
start_time = time.time()
# 측정하고 싶은 코드
result = 0
for i in range(1000000):
result += i
end_time = time.time()
print(f"수행 시간: {end_time - start_time:.5f}초")
수행 시간: 0.06843초
time.time()은 유닉스 타임스탬프(1970년 1월 1일부터의 초)를 반환한다.
시작과 끝에서 각각 찍고 빼면 수행 시간이 나온다.
더 정밀한 측정 : timeit
작은 코드 조각의 성능을 비교할 때는 timeit이 더 정확하다.
import timeit
# 리스트 컴프리헨션 vs for 반복문 속도 비교
code1 = "[i * 2 for i in range(1000)]"
code2 = """
result = []
for i in range(1000):
result.append(i * 2)
"""
t1 = timeit.timeit(code1, number=10000)
t2 = timeit.timeit(code2, number=10000)
print(f"컴프리헨션: {t1:.4f}초")
print(f"for + append: {t2:.4f}초")
컴프리헨션: 0.3821초
for + append: 0.6514초
리스트 컴프리헨션이 일반적으로 더 빠르다.
코딩테스트에서 시간이 촉박할 때 컴프리헨션을 쓰면 유리한 경우가 있다.
파이썬 성능 개선 팁
다음은 코딩테스트에서 파이썬으로 시간 초과가 날 때 시도해볼 수 있는 방법들이다.
input() 대신 sys.stdin.readline()
input()은 느리다. 입력이 많을 때는 sys.stdin.readline()을 사용한다.
import sys
input = sys.stdin.readline
n = int(input())
data = list(map(int, input().split()))
# 주의: readline은 줄 끝에 \n이 붙어 있다
line = sys.stdin.readline()
line = line.strip() # \n 제거 필요
PyPy vs Python
플랫폼에 따라 PyPy 제출이 가능한 경우가 있다.
PyPy는 JIT 컴파일러를 사용해서 순수 반복문 위주 코드에서 Python보다 3~5배 빠르다.
단, 메모리를 더 많이 쓴다.
재귀 깊이 제한
파이썬의 기본 재귀 깊이 제한은 1000이다.
DFS나 재귀 알고리즘에서 이 제한을 넘으면 RecursionError가 발생한다.
import sys
sys.setrecursionlimit(10000) # 재귀 깊이 제한 늘리기
# RecursionError 예시
def factorial(n):
return n * factorial(n - 1) # 종료 조건 없음
factorial(5)
# RecursionError: maximum recursion depth exceeded
# 올바른 재귀 함수 - 종료 조건 필수
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
print(factorial(10)) # 3628800
자주 하는 실수들
정수 오버플로우는 없지만 속도 문제는 있다
파이썬은 정수 크기에 제한이 없다.
C에서 int 오버플로우를 걱정하는 것과 달리 파이썬에서는 아무리 큰 수도 그냥 계산된다.
print(2 ** 100)
# 1267650600228229401496703205376
대신 숫자가 커지면 연산이 느려진다. 불필요하게 큰 수를 다루지 않도록 주의한다.
반복문 안에서 리스트 수정
반복문으로 순회하면서 리스트를 직접 수정하면 예상치 못한 결과가 나온다.
# 잘못된 예시 - 반복 중 리스트 수정
nums = [1, 2, 3, 4, 5]
for num in nums:
if num % 2 == 0:
nums.remove(num)
print(nums) # [1, 3, 5] 가 되어야 하지만
# 실제: [1, 3, 5] ... 운이 좋으면 맞지만 요소 건너뜀 가능
# 올바른 예시 - 새 리스트로 필터링
nums = [1, 2, 3, 4, 5]
nums = [num for num in nums if num % 2 != 0]
print(nums) # [1, 3, 5]
가변 기본 인수 함정
함수 기본 인수에 리스트나 딕셔너리 같은 가변 객체를 쓰면 호출 간 상태가 공유된다.
# 잘못된 예시
def append_to(element, to=[]):
to.append(element)
return to
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] - 예상은 [2]였는데!
# 올바른 예시
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to
print(append_to(1)) # [1]
print(append_to(2)) # [2]
알고리즘 설계 기초는 문법이나 자료구조보다 먼저 머릿속에 넣어두어야 하는 내용이다.
N 범위 보고 시간복잡도 추산하는 것만 잘 해도 불필요한 삽질을 많이 줄일 수 있다.