코딩테스트 최적화 기법 (Optimization Techniques)
한눈에 보기
시간 초과를 피하는 실전 요령을 모았다. 입력 크기로 목표 복잡도를 역산하는 기준표, 빠른 입출력, 누적 합, 투 포인터와 슬라이딩 윈도우, 가지치기를 정리한다.
최종 업데이트 2026.09.17
알고리즘을 제대로 골랐는데도 시간 초과가 나는 경우가 있다. 복잡도는 맞췄지만 상수가 큰 코드를 썼기 때문이다. 파이썬은 특히 이 격차가 커서, 같은 O(n) 코드라도 어떻게 쓰느냐에 따라 10배 넘게 차이 난다.
이 글은 알고리즘을 바꾸지 않고 코드만 손봐서 통과시키는 기법들을 모았다. 입출력, 자료구조 선택, 문자열 처리, 누적합 — 이 네 가지만 챙겨도 "맞는데 시간 초과"의 대부분이 해결된다.
먼저, 복잡도 감각 잡기
최적화에 들어가기 전에 내 알고리즘이 애초에 가능한지 판단해야 한다. 문제의 n을 보고 허용되는 복잡도를 역산하는 습관이 먼저다.
| n의 크기 | 허용 복잡도 | 대표 알고리즘 |
|---|---|---|
| ≤ 10 | O(n!) | 전체 순열 완전 탐색 |
| ≤ 20 | O(2ⁿ) | 부분집합 탐색, 비트마스크 DP |
| ≤ 500 | O(n³) | 플로이드-워셜 |
| ≤ 5,000 | O(n²) | 이중 반복문 DP, 기본 LIS |
| ≤ 100,000 | O(n log n) | 정렬, 이분 탐색, 힙, 다익스트라 |
| ≤ 1,000,000 | O(n) | 한 번 순회, 누적합, 투 포인터 |
| 그 이상 | O(log n), O(1) | 이분 탐색, 수식으로 바로 계산 |
기준은 대략 1초에 1억 번 연산이다. 다만 파이썬은 C++보다 10~50배 느려서 실제로는 1초에 1,000만~2,000만 번 정도로 보는 편이 안전하다. 위 표에서 한 칸 보수적으로 잡으면 된다.
입출력이 병목이다
파이썬에서 가장 흔하고 가장 쉽게 고치는 문제다. 입력이 10만 줄만 넘어가도 input()은 눈에 띄게 느려진다.
import sys
input = sys.stdin.readline # 이 한 줄로 입력이 크게 빨라진다
n = int(input())
arr = list(map(int, input().split()))
# 출력도 마찬가지 — 반복문 안의 print는 매번 버퍼를 비운다
# 나쁜 예
for x in result:
print(x)
# 좋은 예 — 한 번에 모아서 출력
sys.stdout.write('\n'.join(map(str, result)) + '\n')
# 또는
print('\n'.join(map(str, result)))
sys.stdin.readline은 개행 문자를 포함해서 읽는다는 점만 주의한다. 문자열을 그대로 쓸 때는 .rstrip()이 필요하고, int()나 .split()을 거치면 알아서 사라지므로 신경 쓰지 않아도 된다.
자료구조를 잘못 고르면 복잡도가 바뀐다
이건 상수 문제가 아니라 복잡도 자체가 달라지는 문제라 더 치명적이다.
| 작업 | list | set / dict | deque |
|---|---|---|---|
| 인덱스 접근 | O(1) | — | 양 끝만 O(1) |
포함 확인 x in a | O(n) | O(1) | O(n) |
| 맨 뒤 추가·삭제 | O(1) | — | O(1) |
| 맨 앞 추가·삭제 | O(n) | — | O(1) |
| 중간 삭제 | O(n) | O(1) | O(n) |
실전에서 사고가 나는 두 지점이 굵게 표시한 곳이다.
# 1) 반복문 안에서 리스트에 in 을 쓴다 → 전체 O(n²)
visited = []
for x in data:
if x not in visited: # 매번 O(n)
visited.append(x)
visited = set() # 이렇게 바꾸면 O(n)
for x in data:
if x not in visited: # O(1)
visited.add(x)
# 2) BFS 큐를 리스트로 만든다 → 전체 O(V²)
from collections import deque
q = []
q.pop(0) # O(n) — 뒤의 원소를 전부 앞으로 당긴다
q = deque()
q.popleft() # O(1)
문자열은 더하지 말고 모았다가 합친다
파이썬 문자열은 불변이라 += 할 때마다 새 문자열을 통째로 만들어 복사한다. 반복문 안에서 쓰면 O(n²)이 된다.
join은 결과 크기를 미리 알고 한 번에 할당하므로 조각마다 한 번씩만 복사한다.# 나쁜 예 — O(n²)
s = ''
for x in items:
s += str(x)
# 좋은 예 — O(n)
s = ''.join(str(x) for x in items)
같은 구간을 반복해서 더하지 않는다 — 누적합
"구간 [l, r]의 합"을 여러 번 물어보는 문제에서, 매번 그 구간을 더하면 질의 하나에 O(n)이다. 미리 앞에서부터의 합을 한 번 계산해두면 질의가 뺄셈 한 번, 즉 O(1)이 된다.
l이 0일 때도 예외 없이 prefix[r+1] − prefix[l] 한 식으로 처리된다.from itertools import accumulate
arr = [3, 1, 4, 1, 5, 9, 2, 6]
prefix = [0] + list(accumulate(arr)) # [0, 3, 4, 8, 9, 14, 23, 25, 31]
def range_sum(l, r): # 구간 [l, r] 합, 양끝 포함
return prefix[r + 1] - prefix[l]
range_sum(2, 5) # 19
이중 반복문을 한 번의 순회로 — 투 포인터와 슬라이딩 윈도우
"모든 구간을 다 확인한다"는 발상은 O(n²)이다. 그런데 구간을 조금씩 옮길 때 이전 계산 결과를 재활용할 수 있다면 전체를 한 번 순회하는 것으로 끝난다.
슬라이딩 윈도우는 길이가 고정된 구간을 옮기는 경우다. 구간을 한 칸 밀 때 매번 전부 더할 필요 없이, 빠지는 값을 빼고 들어오는 값을 더하면 된다. 구간 길이가 얼마든 연산 두 번이다.
def max_window_sum(arr, k):
"""길이 k인 연속 구간의 최대 합."""
window = sum(arr[:k]) # 첫 윈도우만 직접 더한다
best = window
for i in range(k, len(arr)):
window += arr[i] - arr[i - k] # 들어오는 값 더하고 빠지는 값 뺀다
best = max(best, window)
return best
투 포인터는 구간의 길이가 고정되지 않은 경우다. 조건에 따라 왼쪽이나 오른쪽 포인터를 옮기는데, 두 포인터가 모두 뒤로만 가므로 전체 이동 횟수가 2n을 넘지 않는다. 그래서 O(n)이다.
def two_sum_sorted(arr, target):
"""정렬된 배열에서 합이 target인 두 수를 찾는다."""
l, r = 0, len(arr) - 1
while l < r:
s = arr[l] + arr[r]
if s == target:
return arr[l], arr[r]
if s < target:
l += 1 # 합을 키워야 하니 왼쪽을 올린다
else:
r -= 1 # 합을 줄여야 하니 오른쪽을 내린다
return None
def min_length_subarray(arr, target):
"""합이 target 이상인 가장 짧은 연속 구간의 길이. 길이가 가변인 투 포인터."""
l, total, best = 0, 0, float('inf')
for r, x in enumerate(arr):
total += x
while total >= target: # 조건을 만족하는 동안 왼쪽을 좁힌다
best = min(best, r - l + 1)
total -= arr[l]
l += 1
return best if best != float('inf') else 0
두 번째 예시의 while이 안쪽에 있어 O(n²)처럼 보이지만 아니다. l은 전체를 통틀어 최대 n번만 증가하므로 안쪽 반복의 총 실행 횟수도 n번이다. 이렇게 중첩 반복문의 총 실행 횟수를 따로 세는 감각이 투 포인터를 이해하는 열쇠다.
투 포인터를 쓰려면 조건이 단조적이어야 한다. "왼쪽을 옮기면 값이 반드시 줄고, 오른쪽을 옮기면 반드시 는다"는 성질이 없으면 포인터를 되돌려야 해서 성립하지 않는다. 음수가 섞인 배열에서 위 코드가 깨지는 이유가 이것이다.
반복문 안에서 하지 말아야 할 것들
# 매번 정렬한다
for q in queries:
arr.sort() # 루프 밖으로 빼면 한 번이면 된다
# 매번 len()을 부른다 (사소하지만 큰 루프에서는 쌓인다)
for i in range(len(arr)):
for j in range(len(arr)):
...
n = len(arr) # 미리 꺼내둔다
# 매번 리스트를 새로 만든다
for x in data:
temp = list(big_list) # 복사가 O(n)
# 전역 변수를 반복 참조한다 — 지역 변수가 더 빠르다
def solve():
local_fn = some_function # 자주 부르는 함수는 지역에 묶어둔다
for x in data:
local_fn(x)
알아두면 좋은 표준 라이브러리
| 상황 | 도구 | 대체하는 것 |
|---|---|---|
| 개수 세기 | collections.Counter | dict 수동 카운팅 |
| 기본값 있는 dict | collections.defaultdict | if key not in d 검사 |
| 양쪽 끝 큐 | collections.deque | list.pop(0) |
| 우선순위 큐 | heapq | 매번 정렬 |
| 정렬된 배열 삽입 위치 | bisect | 직접 이분 탐색 |
| 누적합 | itertools.accumulate | 반복문 |
| 순열·조합 | itertools | 직접 백트래킹 |
| 재귀 결과 캐싱 | functools.lru_cache | 수동 메모이제이션 |
내장 함수와 라이브러리는 C로 구현되어 있어서, 같은 일을 하는 파이썬 반복문보다 훨씬 빠르다. sum(arr)이 직접 더하는 반복문보다 빠른 이유다. 직접 구현할 이유가 없다면 라이브러리를 쓴다.
자주 하는 실수
- 알고리즘이 틀렸는데 상수 최적화로 해결하려 한다. O(n²)을 아무리 다듬어도 n이 10만이면 통과하지 못한다. 시간 초과가 나면 먼저 복잡도가 맞는지 확인하고, 맞다면 그때 이 글의 기법들을 적용한다.
- 리스트에
in을 쓴다. 가장 흔한 숨은 O(n²)이다. 포함 확인이 목적이면 무조건set이다. list.pop(0)을 쓴다. BFS에서 이것 하나로 시간 초과가 난다.deque를 쓴다.- 반복문 안에서 문자열을 더한다. 출력이 많은 문제에서 특히 치명적이다.
input()을 그대로 쓴다. 입력 줄이 많으면sys.stdin.readline으로 바꾼다.- 누적합 배열의 인덱스를 헷갈린다. 맨 앞에 0을 넣지 않으면
l = 0일 때 예외 처리가 필요해지고, 거기서 실수가 난다. - 재귀 한도를 안 올린다. DFS나 DP 재귀에서
sys.setrecursionlimit(10**6)은 습관적으로 넣어두는 편이 좋다.
면접에서 이렇게 물어본다
- 파이썬에서 리스트와 셋의 포함 확인 복잡도가 다른 이유는 무엇인가.
list.pop(0)이 O(n)인 이유와 대안을 설명해 보라.- 반복문에서 문자열을
+=로 이어붙이면 왜 느린가. - 누적합이 어떤 문제를 어떻게 개선하는지 설명해 보라.
- n의 크기를 보고 알고리즘을 고르는 기준을 말해 보라.
- 시간 초과가 났을 때 어떤 순서로 원인을 찾겠는가.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- DFS와 BFS (Depth First Search, Breadth First Search)그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
- 다익스트라 알고리즘 (Dijkstra's Algorithm)음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기