코딩테스트 최적화 기법 (Optimization Techniques)

한눈에 보기

시간 초과를 피하는 실전 요령을 모았다. 입력 크기로 목표 복잡도를 역산하는 기준표, 빠른 입출력, 누적 합, 투 포인터와 슬라이딩 윈도우, 가지치기를 정리한다.

최종 업데이트 2026.09.17

알고리즘을 제대로 골랐는데도 시간 초과가 나는 경우가 있다. 복잡도는 맞췄지만 상수가 큰 코드를 썼기 때문이다. 파이썬은 특히 이 격차가 커서, 같은 O(n) 코드라도 어떻게 쓰느냐에 따라 10배 넘게 차이 난다.

이 글은 알고리즘을 바꾸지 않고 코드만 손봐서 통과시키는 기법들을 모았다. 입출력, 자료구조 선택, 문자열 처리, 누적합 — 이 네 가지만 챙겨도 "맞는데 시간 초과"의 대부분이 해결된다.

먼저, 복잡도 감각 잡기

최적화에 들어가기 전에 내 알고리즘이 애초에 가능한지 판단해야 한다. 문제의 n을 보고 허용되는 복잡도를 역산하는 습관이 먼저다.

n의 크기허용 복잡도대표 알고리즘
≤ 10O(n!)전체 순열 완전 탐색
≤ 20O(2ⁿ)부분집합 탐색, 비트마스크 DP
≤ 500O(n³)플로이드-워셜
≤ 5,000O(n²)이중 반복문 DP, 기본 LIS
≤ 100,000O(n log n)정렬, 이분 탐색, 힙, 다익스트라
≤ 1,000,000O(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()을 거치면 알아서 사라지므로 신경 쓰지 않아도 된다.

자료구조를 잘못 고르면 복잡도가 바뀐다

이건 상수 문제가 아니라 복잡도 자체가 달라지는 문제라 더 치명적이다.

작업listset / dictdeque
인덱스 접근O(1)—양 끝만 O(1)
포함 확인 x in aO(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으로 한 번에 합치면 선형 시간이 되는 비교
왼쪽 막대가 각 단계에서 복사되는 양이다. 길이가 계속 늘어나므로 총합이 제곱에 비례한다. 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)이 된다.

코딩테스트 최적화: 누적합 배열을 이용해 구간 합을 뺄셈 한 번으로 구하는 그림
누적합 배열은 원본보다 한 칸 길고 맨 앞이 0이다. 그래야 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²)이다. 그런데 구간을 조금씩 옮길 때 이전 계산 결과를 재활용할 수 있다면 전체를 한 번 순회하는 것으로 끝난다.

슬라이딩 윈도우는 길이가 고정된 구간을 옮기는 경우다. 구간을 한 칸 밀 때 매번 전부 더할 필요 없이, 빠지는 값을 빼고 들어오는 값을 더하면 된다. 구간 길이가 얼마든 연산 두 번이다.

코딩테스트 최적화: 길이 3인 윈도우를 한 칸 옮길 때 빠지는 값을 빼고 들어오는 값만 더하면 되는 그림
윈도우를 옮길 때마다 구간을 처음부터 더하면 O(nk)지만, 빠지는 값과 들어오는 값만 반영하면 O(n)이다. k가 클수록 격차가 벌어진다.
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.Counterdict 수동 카운팅
기본값 있는 dictcollections.defaultdictif key not in d 검사
양쪽 끝 큐collections.dequelist.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의 크기를 보고 알고리즘을 고르는 기준을 말해 보라.
  • 시간 초과가 났을 때 어떤 순서로 원인을 찾겠는가.
알고리즘 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기