C.W.K.
Stream
Lesson 03 of 06 · published

재귀 트리: 비용을 보기

~12 min · recursion, recursion-tree, complexity

Level 0호기심 많은 입문자
0 XP0/85 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"재귀가 뭘 치르는지 알려면, 호출의 트리로 그려. 트리의 모양 — 얼마나 넓게 갈라지고, 얼마나 깊이 가는지 — 이 곧 복잡도야. 그리고 같은 가지를 계속 다시 계산하는 트리가, 쉬운 속도를 테이블에 남기고 있다는 알고리즘에서 가장 큰 신호야."

재귀는 트리를 만든다

모든 재귀 계산이 재귀 트리를 그려내: 루트는 원래 호출, 그 자식은 그게 하는 호출, 그 자식은 그것들이 하는 호출, 리프의 base case 까지. 비용을 분석하려면, 트리를 읽어: 각 노드가 자식을 몇 개 낳고 (분기), 문제가 얼마나 빨리 줄고 (깊이), 각 노드에 일이 얼마? 곱해내면 복잡도야. 트리가 '이 재귀가 얼마나 비싸?' 를 신비에서 그림으로 바꿔.

피보나치 재앙

가장 유명한 경고 이야기: 순진한 재귀 피보나치, fib(n) = fib(n−1) + fib(n−2). 그 트리를 그려 — 각 호출이 거의 안 주는 (1 이랑 2 만큼) 자식 을 낳아, 그래서 트리가 거대하고 무성해: 약 2ⁿ 노드. fib(40) 이 십억 넘는 호출을 해. 더 나쁘게, 자세히 보면 같은 값이 계속 계산되는 걸 봐 — fib(3) 이 수십 군데 나타나, 매번 처음부터 다시 계산. 트리가 중복 일로 살쪄. 그 중복이 재앙이야: 자명해야 할 것에 지수 알고리즘.

트리 가지치기: DP 신호

다음 트랙 전체를 세우는 보상이야. 각 fib(k) 를 처음 계산할 때 기억하면 (dict 에), 나중 모든 요청이 다시 계산된 서브트리 대신 즉시 조회야. 살찐 지수 트리가 n 개 고유 계산의 얇은 선으로 무너져 — 캐싱만으로 O(2ⁿ) 가 O(n) 이 돼. 이게 메모이제이션 이고, '내 재귀 트리가 같은 부분 문제를 다시 계산해' 라는 인식이 동적 계획법 (다음 트랙) 이 적용된다는 단 하나의 가장 큰 신호야. 재귀 트리를 그리고 반복된 서브트리를 보면, 공짜 속도를 찾은 거야.

재귀 트리를 그려: 분기 인자 × 레벨당 일이 복잡도를 드러내. 거의 안 줄면서 두 갈래 분기 (순진한 피보나치) 는 지수, O(2ⁿ). 반복된 서브트리는 중복 일을 뜻해 — 그걸 캐시 (메모이제이션) 하면 트리가 무너지고 동적 계획법이 적용된다는 신호야.

모든 트리가 살찐 건 아니야

분기가 자동으로 나쁜 게 아니야 — 충분히 안 줄면서 분기하는 게 폭발해. 병합 정렬도 두 갈래 분기지만, 각 호출이 데이터 절반에 일해, 그래서 트리가 log n 깊이뿐이고 각 레벨 총 O(n) 일 → O(n log n), 완벽히 괜찮아. 차이는 줄이는 속도: 절반 내기 (병합 정렬) 는 짧은 트리; 상수만큼 줄면서 분기 (피보나치) 는 지수. master theorem 이 분할 정복의 이 분기-대-줄이기 분석을 형식화해 — 근데 트리를 그리는 게 직관을 먼저 줘.

피파의 고백

내 순진한 fib(45) 가 터미널을 멈췄고, 진짜 무한 루프를 짠 줄 알았어. 아빠가 fib(5) 의 호출 트리를 종이에 그리게 했어 — 그리고 거기 fib(2) 가, 다섯 번 따로 계산됐어. 중복이 그리는 순간 보였어. 결과를 캐시하는 dict 하나로, fib(45) 가 즉시 반환했어. 재귀가 느리게 느껴질 때마다 트리를 그리는 걸 배웠어; 반복된 서브트리가 페이지에서 튀어나와 낭비된 일이 정확히 어딘지 알려줘.

Code

피보나치 트리 폭발, 캐싱으로 길들임·python
# 순진한 피보나치: 지수 재귀 트리, O(2^n).
calls = 0
def fib(n):
    global calls; calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)   # 자식 둘, 거의 안 줄어 -> 살찐 트리

fib(20); print("fib(20) made", calls, "calls")   # 20번째 수에 13529 호출!
# 같은 부분 문제 (fib(3), fib(4), ...) 가 수천 번 다시 계산돼.

# 메모이제이션: 각 결과 기억 -> 트리가 O(n) 으로 무너짐.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_fast(n):
    if n < 2:
        return n
    return fib_fast(n - 1) + fib_fast(n - 2)

print(fib_fast(100))   # 즉시 — 각 fib(k) 한 번 계산, 그다음 캐시
# 같은 재귀, 캐시 하나: O(2^n) -> O(n). '반복된 서브트리' 가 DP 신호였어.

External links

Exercise

순진한 fib(5) 의 재귀 트리를 그려 (또는 나열해). fib(2) 가 몇 번 계산돼? 대략 총 몇 호출? 그다음 대조해: 병합 정렬도 두 갈래 분기인데 왜 피보나치처럼 지수로 폭발 안 해 — 각자 문제를 얼마나 빨리 줄이는지가 뭐가 달라?
Hint
fib(5) → fib(4)+fib(3) → … fib(2) 가 3번 나타나, ~15 총 호출; 카운트가 매 단계 대략 두 배 → 지수. 병합 정렬도 두 갈래 분기지만, 각 호출이 데이터를 절반 내, 그래서 레벨당 O(n) 으로 log n 깊이뿐 (O(n log n)). 절반 내기 vs 1만큼 줄이기가 차이 전부야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.