재귀와 반복의 트레이드오프
재귀적인 팩토리얼과 피보나치를 반복문으로 변환하고, Python의 재귀 한도와 스택 크기 때문에 반복 방식이 더 적합한 경우를 설명합니다.
재귀와 반복의 트레이드오프은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
재귀와 반복의 이중성
재귀적으로 작성할 수 있는 모든 알고리즘은 반복적으로도 작성할 수 있고, 그 반대도 마찬가지입니다. 재귀형은 문제의 수학적 정의를 더 가깝게 반영하는 경우가 많고, 반복형은 메모리를 명시적으로 제어하며 스택 오버플로 위험을 피할 수 있습니다. 둘 중 무엇을 선택할지는 가독성, 깊이 제한, 성능 요구 사항을 바탕으로 결정하는 실용적인 문제입니다.
면접에서 두 버전을 모두 제시하고 장단점을 설명할 수 있다면 높은 숙련도를 보여 주는 강력한 신호가 됩니다.
팩토리얼: 재귀형과 반복형 비교
팩토리얼은 대표적인 예입니다. 재귀형은 수학적 정의 n! = n × (n-1)!을 직접 표현합니다. n개의 반환 값이 처리 대기 상태로 남기 때문에 O(n) 스택 공간을 사용합니다. 반복형은 1부터 n까지 반복하며 O(1) 공간을 사용합니다. n = 1000이면 재귀형은 Python의 기본 제한에 도달하지만, 반복형은 임의로 큰 n도 처리할 수 있습니다.
def factorial_rec(n):
if n == 0:
return 1
return n * factorial_rec(n - 1) # O(n) stack
def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i # O(1) stack
return result
print(factorial_rec(10)) # 3628800
print(factorial_iter(10)) # 3628800
# Large n: iterative works, recursive may overflow
print(factorial_iter(1000) > 0) # True (Python handles big ints)피보나치: 지수 시간과 선형 시간
순진한 재귀 피보나치는 O(2^n) time 복잡도를 가지므로 큰 n에서는 매우 느립니다. 반복형은 O(n) time 복잡도와 O(1) 공간을 사용합니다. 메모이제이션을 적용한 재귀(다음 단원)는 O(n) time 복잡도를 가지지만, 메모 딕셔너리와 O(n) 스택 때문에 O(n) 공간을 사용합니다. 피보나치에서는 모든 지표에서 반복 방식이 최적입니다. n = 50이면 순진한 재귀는 수 초가 걸리지만, 반복형은 마이크로초가 걸립니다.
import time
def fib_rec(n):
if n <= 1: return n
return fib_rec(n-1) + fib_rec(n-2) # O(2^n)
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a # O(n) time, O(1) space
# Timing comparison for n=35
start = time.time()
fib_rec(35)
print(f'Recursive n=35: {time.time()-start:.3f}s')
start = time.time()
fib_iter(35)
print(f'Iterative n=35: {time.time()-start:.6f}s')
print(fib_iter(100)) # handles large n트리 순회: 재귀 방식과 반복 방식
재귀 트리 순회는 트리 구조가 재귀와 자연스럽게 대응하므로 깔끔하게 작성할 수 있습니다. 하지만 매우 치우친 트리, 즉 사실상 연결 리스트와 같은 트리에서는 재귀 깊이가 트리 높이와 같아져 O(n)이 되므로 스택 오버플로가 발생할 위험이 있습니다. 명시적 스택을 사용하는 반복 방식은 깊이 제한이 없으며, 호출 스택이 아니라 힙에서 스택 크기를 확장할 수 있습니다.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val; self.left = left; self.right = right
def preorder_rec(root, result=None):
if result is None: result = []
if root:
result.append(root.val)
preorder_rec(root.left, result)
preorder_rec(root.right, result)
return result
def preorder_iter(root):
if not root: return []
result, stack = [], [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
return result
root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(preorder_rec(root)) # [1, 2, 4, 5, 3]
print(preorder_iter(root)) # [1, 2, 4, 5, 3]병합 정렬: 재귀 방식과 반복 방식(상향식)
병합 정렬은 분할하고, 재귀 호출한 다음, 병합하는 방식이므로 자연스럽게 재귀적으로 구현됩니다. 반복 방식의 상향식 병합 정렬은 재귀를 완전히 사용하지 않습니다. 크기가 1인 부분 배열에서 시작하여 인접한 쌍을 크기 2인 부분 배열로 병합하고, 이어서 크기 4인 부분 배열로 병합하는 식으로 각 단계마다 부분 배열의 크기를 두 배로 늘립니다. 상향식 병합 정렬의 시간 복잡도는 O(n), 공간 복잡도는 병합 버퍼에 필요한 O(n), 스택 공간은 O(1)입니다.
def merge_sort_iterative(arr):
n = len(arr)
size = 1
while size < n:
for start in range(0, n, 2 * size):
mid = min(start + size, n)
end = min(start + 2 * size, n)
left = arr[start:mid]
right = arr[mid:end]
# Merge
i = j = 0
for k in range(start, end):
if i < len(left) and (j >= len(right) or left[i] <= right[j]):
arr[k] = left[i]; i += 1
else:
arr[k] = right[j]; j += 1
size *= 2
return arr
print(merge_sort_iterative([5, 2, 4, 6, 1, 3])) # [1,2,3,4,5,6]재귀 방식이 확실히 더 나은 경우
문제가 호출 그래프에 직접 대응하는 트리와 같은 구조를 가지며, 기저 사례가 자연스럽고, 깊이가 제한되어 있을 때 재귀가 특히 유리합니다. 예를 들어 균형 트리와 분할 정복에서는 깊이가 O(log n)으로 제한됩니다. JSON 구문 분석, 디렉터리 순회, 게임 트리, 되돌리기 문제 등이 그 예입니다. 이러한 경우 재귀 코드는 이에 상응하는 반복 코드보다 짧고 명확하며, 올바름을 증명하기도 쉽습니다.
# Recursion is clearest for JSON-like nested structures
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item)) # recurse on sub-list
else:
result.append(item)
return result
print(flatten([1, [2, [3, 4], 5], 6])) # [1, 2, 3, 4, 5, 6]
print(flatten([])) # []
print(flatten([[1, [2]], [3, [4, [5]]]])) # [1, 2, 3, 4, 5]반복 방식이 확실히 더 나은 경우
다음과 같은 경우에는 반복 방식을 선택하는 것이 좋습니다. 깊이가 O(n)이고 n이 큰 경우(안전한 Python 코드에서는 약 500보다 큰 경우), 재귀 방식과 반복 방식의 가독성이 같은 경우(피보나치, 계승), 또는 자연스러운 하위 문제 분해가 없는 근본적으로 순차적인 문제인 경우입니다. 배열을 왼쪽에서 오른쪽으로 처리하는 단순한 반복문, 즉 누적 합, 슬라이딩 윈도, 투 포인터는 항상 반복 방식으로 구현해야 합니다.
# Iterative is clearest for sequential array processing
def running_max(nums):
result = []
curr_max = float('-inf')
for n in nums:
curr_max = max(curr_max, n)
result.append(curr_max)
return result
print(running_max([3, 1, 4, 1, 5, 9, 2, 6])) # [3,3,4,4,5,9,9,9]
# No natural recursion here — iteration is the only sensible choiceDFS 재귀를 반복 방식으로 변환하기
체계적인 방법은 다음과 같습니다. 모든 재귀 DFS는 재귀 호출의 인수를 명시적 스택에 넣는 방식으로 반복 방식으로 변환할 수 있습니다. 핵심은 재귀 호출 f(args)가 args를 스택에 넣고 반복하는 것과 같다는 점입니다. 부모를 처리하기 전에 자식의 결과가 필요한 후위 순서 처리에서는 두 번 순회하는 방식이나 방문 여부 표시가 필요할 수 있습니다.
# Post-order iterative using two stacks
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val=val; self.left=left; self.right=right
def postorder_iter(root):
if not root: return []
s1, s2 = [root], []
while s1:
node = s1.pop()
s2.append(node.val)
if node.left: s1.append(node.left)
if node.right: s1.append(node.right)
return s2[::-1] # reverse gives post-order
root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(postorder_iter(root)) # [4, 5, 2, 3, 1]재귀의 성능 오버헤드
Python에서 재귀 호출마다 무시할 수 없는 비용이 발생합니다. 새 프레임이 생성되고 힙에 메모리가 할당되며, 지역 변수가 초기화되고 반환 주소 포인터가 저장됩니다. 성능 측정 결과 Python의 함수 호출 비용은 호출당 대략 100~200나노초입니다. 재귀 깊이가 10^6이면 알고리즘의 실제 작업과 관계없이 순수한 추가 비용만 0.1~0.2초에 이릅니다. 반복문은 이러한 비용을 완전히 피할 수 있습니다.
import time
def rec_sum(n):
if n == 0: return 0
return n + rec_sum(n - 1)
def iter_sum(n):
total = 0
for i in range(n + 1):
total += i
return total
import sys; sys.setrecursionlimit(10000)
n = 5000
start = time.time()
for _ in range(100): rec_sum(n)
print(f'Recursive sum({n}) x100: {(time.time()-start)*1000:.2f}ms')
start = time.time()
for _ in range(100): iter_sum(n)
print(f'Iterative sum({n}) x100: {(time.time()-start)*1000:.2f}ms')면접에서 선택하기
코딩 면접에서 선택할 수 있는 경우 다음과 같이 질문해 보세요. '재귀 깊이가 O(log n)으로 제한되어 있는가?' 그렇다면 재귀를 사용해도 괜찮습니다. '재귀 깊이가 O(n)인가?' 그렇다면 반복 방식을 우선하거나, 실제 서비스 코드에서는 반복 방식으로 변환하겠다고 언급하는 것이 좋습니다. '문제가 본질적으로 트리 구조이거나 분할 정복 형태인가?' 그렇다면 재귀 방식에 무게를 두세요. '문제가 순차적인 탐색인가?' 그렇다면 반복 방식을 사용하세요.
항상 판단 근거를 밝혀야 합니다. '여기서는 균형 BST에서 깊이가 O(log n)이므로 O(log n)의 스택 공간을 허용할 수 있기 때문에 재귀를 사용하겠습니다.'
요약: 상충 관계 표
상충 관계를 요약하면 다음과 같습니다. 재귀 코드는 대체로 더 짧고 문제 구조를 그대로 반영하지만, 깊이에 비례하는 O(깊이)의 스택 공간을 사용하며 함수 호출 비용이 발생합니다. 반복 코드는 더 길지만 O(1)의 스택 공간을 사용하고 재귀 깊이 제한을 피합니다. 메모이제이션을 적용한 재귀 방식(다음 수업에서 다룹니다)은 그 중간 지점으로, 재귀의 명확성을 유지하면서 중복 계산을 없앱니다. 해법을 분석할 때는 호출 스택 공간을 포함한 공간 복잡도를 항상 명확히 밝혀야 합니다.
rows = [
('Factorial', 'O(n) / O(1)', 'O(n) / O(1)', 'Same time; iter wins on space'),
('Fibonacci', 'O(2^n) / O(n)', 'O(n) / O(1)', 'Iter massively wins'),
('Binary search','O(log n) / O(log n)', 'O(log n) / O(1)', 'Iter wins on space'),
('Tree DFS', 'O(n) / O(h)', 'O(n) / O(h)', 'Equal; rec cleaner'),
('Merge sort', 'O(n log n) / O(log n)', 'O(n log n) / O(1)', 'BU-iter wins on stack'),
]
for name, rec, it, note in rows:
print(f'{name:<15} rec={rec:<22} iter={it:<22} {note}')간단 확인
이 수업에서 배운 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보세요.
수업 복습
이 수업에서는 다음을 배웠습니다. 깊이가 O(log n)이거나 문제가 본질적으로 트리 구조일 때는 재귀 방식을, 깊이가 O(n)이거나 문제가 순차적일 때는 반복 방식을 우선합니다. 또한 순진한 재귀 피보나치는 O(2^n)이며 반복 방식은 시간 복잡도 O(n), 공간 복잡도 O(1)입니다. 그리고 모든 재귀 DFS는 힙에 명시적 스택을 관리하는 방식으로 반복 방식으로 변환할 수 있습니다. 다음으로 메모이제이션을 적용하여 중복 재귀 호출을 없애 보겠습니다.
자주 묻는 질문
“재귀와 반복의 트레이드오프” 강의는 무료인가요?
네 — “재귀와 반복의 트레이드오프” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“재귀와 반복의 트레이드오프”에서 뭘 배우나요?
재귀적인 팩토리얼과 피보나치를 반복문으로 변환하고, Python의 재귀 한도와 스택 크기 때문에 반복 방식이 더 적합한 경우를 설명합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“재귀와 반복의 트레이드오프” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 재귀 프레임워크: 기본 사례, 신뢰, 구성
- 호출 스택 시각화
- 재귀와 반복의 트레이드오프
- 메모이제이션: 재귀 결과 캐싱