재귀 프레임워크: 기본 사례, 신뢰, 구성
모든 호출을 추적하지 않고 팩토리얼, 거듭제곱, 자릿수 합의 올바른 재귀 해법을 작성하는 세 단계 방법을 적용합니다.
재귀 프레임워크: 기본 사례, 신뢰, 구성은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
재귀가 어려운 이유
대부분의 초보자는 모든 재귀 호출을 머릿속으로 추적하려고 합니다. 하지만 재귀가 5단계만 깊어져도 이는 빠르게 감당하기 어려워집니다. 전문가들은 세 단계 프레임워크인 기본 사례, 신뢰, 구축을 사용합니다. 이 방법을 사용하면 전체 호출 트리를 머릿속으로 시뮬레이션하지 않고도 올바른 재귀 함수를 작성할 수 있습니다.
이 프레임워크는 때때로 믿음의 도약이라고도 합니다. 함수가 더 작은 입력에 대해 올바르게 작동한다고 믿고, 그 가정을 바탕으로 더 큰 입력에 대한 해법을 구축하기 때문입니다.
1단계: 기저 조건 정의
기저 조건은 추가 재귀 없이도 답을 알고 있는 가장 간단한 입력입니다. 모든 재귀 함수에는 하나 이상의 기저 조건이 있어야 합니다. 기저 조건이 없으면 함수가 무한히 재귀합니다(스택 오버플로). 좋은 기저 조건의 예로는 빈 목록, 원소 하나, n == 0, n == 1, 또는 문제가 자명한 항등식으로 축소되는 경우가 있습니다.
재귀 로직을 작성하기 전에 기저 조건을 먼저 작성하십시오. ‘추가 설명 없이 즉시 답할 수 있는 이 문제의 가장 작은 형태는 무엇인가?’라고 물어 기저 조건을 찾으십시오.
# Base cases for common problems
def factorial(n):
if n == 0: # base case: 0! = 1
return 1
# ... recursive step below
def sum_list(lst):
if not lst: # base case: sum of empty list is 0
return 0
# ...
def height(node):
if node is None: # base case: height of null node is 0
return 0
# ...
print('Base cases identified')2단계: 재귀 호출 신뢰하기
신뢰 단계는 믿음의 도약입니다. 현재 입력보다 엄격히 작은 모든 입력에 대해 함수가 이미 올바르게 작동한다고 가정합니다. 지금 당장 더 작은 모든 입력에 대해 이를 증명할 필요는 없습니다. 귀납적 증명이 이를 보장합니다. 단순히 더 작은 하위 문제에 함수를 호출하고 올바른 결과를 반환한다고 신뢰하십시오.
초보자는 이 단계를 건너뛰고 대신 머릿속으로 시뮬레이션하려고 합니다. 그런 충동을 억누르십시오. 이 틀을 체화하면 임의로 깊은 재귀에도 확장할 수 있습니다.
# Trust example: sum_list([3, 1, 4, 1, 5])
# Trust: sum_list([1, 4, 1, 5]) = 11 (we TRUST this, don't trace it)
# Build: 3 + 11 = 14
# So:
def sum_list(lst):
if not lst:
return 0
# Trust that sum_list(lst[1:]) returns sum of the rest
return lst[0] + sum_list(lst[1:])
print(sum_list([3, 1, 4, 1, 5])) # 143단계: 해답 구성하기
구성 단계에서는 신뢰한 하위 문제의 결과와 현재 원소의 기여분을 결합하여 전체 입력에 대한 답을 만듭니다. 보통 한 줄이면 충분합니다. 현재 원소와 재귀 호출의 결과에 연산을 적용하는 방식입니다. 일반적인 구성 방법으로는 합에 더하기, 목록 앞에 추가하기, 개수 증가시키기, 두 하위 결과 결합하기가 있습니다.
def factorial(n):
if n == 0:
return 1
# Trust: factorial(n-1) gives (n-1)!
# Build: n * (n-1)! = n!
return n * factorial(n - 1)
def power(base, exp):
if exp == 0:
return 1
# Trust: power(base, exp-1) gives base^(exp-1)
# Build: base * base^(exp-1) = base^exp
return base * power(base, exp - 1)
print(factorial(6)) # 720
print(power(2, 10)) # 1024자릿수의 합에 틀 적용하기
문제: 음이 아닌 정수의 자릿수 합을 계산합니다. 기저 조건: n == 0 → 합은 0입니다(또는 n < 10 → n 자체입니다). 신뢰: sumDigits(n // 10)은 마지막 자리를 제외한 모든 자릿수의 합을 반환합니다. 구성: 마지막 자릿수 n % 10을 신뢰한 결과에 더합니다. 이 틀은 세 가지 선언적 단계로 해답을 만들어 냅니다.
def sumDigits(n):
if n < 10:
return n # base case: single digit
# Trust: sumDigits(n // 10) gives sum of all digits except last
# Build: add the last digit
return n % 10 + sumDigits(n // 10)
print(sumDigits(0)) # 0
print(sumDigits(7)) # 7
print(sumDigits(123)) # 6
print(sumDigits(9999)) # 36피보나치: 두 하위 문제
피보나치는 두 번의 재귀 호출을 필요로 합니다: fib(n-1)과 fib(n-2)입니다. 틀을 적용해 보겠습니다. 기저 조건은 fib(0) = 0과 fib(1) = 1입니다. 신뢰: 두 작은 입력에 대한 호출이 올바른 피보나치 값을 반환한다고 가정합니다. 구성: 두 값을 더한 결과를 반환합니다. 이 순진한 구현의 시간 복잡도는 O(2^n)입니다. 메모이제이션 단원에서 이를 개선하겠습니다.
def fib(n):
if n <= 1:
return n # base cases: fib(0)=0, fib(1)=1
# Trust both smaller sub-problems
return fib(n - 1) + fib(n - 2)
for i in range(8):
print(f'fib({i}) = {fib(i)}') # 0,1,1,2,3,5,8,13문자열을 재귀적으로 뒤집기
문제: 문자열을 재귀적으로 뒤집습니다. 기저 조건: 빈 문자열 또는 문자 하나인 문자열은 이미 뒤집힌 상태입니다. 신뢰: reverse(s[1:])은 첫 문자를 제외한 나머지 부분을 뒤집은 결과를 반환합니다. 구성: 첫 문자를 뒤집힌 접미 문자열의 끝에 append합니다. 이 틀을 사용하면 세 줄로 해답을 작성할 수 있습니다.
def reverse_str(s):
if len(s) <= 1:
return s # base case
# Trust: reverse_str(s[1:]) = reverse of 'ello' for 'hello'
# Build: append first character at end
return reverse_str(s[1:]) + s[0]
print(reverse_str('')) # ''
print(reverse_str('a')) # 'a'
print(reverse_str('hello')) # 'olleh'
print(reverse_str('racecar')) # 'racecar'재귀적으로 출현 횟수 세기
문제: 목록에서 대상 값이 나타나는 횟수를 재귀적으로 셉니다. 기저 조건: 빈 목록의 개수는 0입니다. 신뢰: count(lst[1:], target)은 꼬리 부분에서의 개수를 반환합니다. 구성: 첫 원소가 대상과 일치하면 1을 더하고, 그렇지 않으면 0을 더합니다. 모든 재귀 단계는 목록 크기를 1씩 줄여 기저 조건을 향해 진행합니다.
def count_occurrences(lst, target):
if not lst:
return 0
# Trust: count in rest of list is handled recursively
# Build: add 1 if first element matches, else 0
return (1 if lst[0] == target else 0) + count_occurrences(lst[1:], target)
print(count_occurrences([1, 2, 3, 2, 4, 2], 2)) # 3
print(count_occurrences([], 5)) # 0
print(count_occurrences([7, 7, 7], 7)) # 3목록이 정렬되어 있는지 확인하기
문제: 목록이 오름차순으로 정렬되어 있는지 재귀적으로 확인합니다. 기저 조건: 원소가 0개 또는 1개인 목록은 항상 정렬된 상태입니다. 신뢰: is_sorted(lst[1:])이 꼬리 부분의 정렬 여부를 알려 줍니다. 구성: 첫 원소가 두 번째 원소보다 작거나 같고 AND 꼬리 부분도 정렬되어 있으면 목록 전체가 정렬된 상태입니다. 구성 단계에서 두 조건의 논리적 AND를 사용하는 깔끔한 예입니다.
def is_sorted(lst):
if len(lst) <= 1:
return True
# Trust: is_sorted(lst[1:]) tells us if tail is sorted
# Build: head <= second element AND tail is sorted
return lst[0] <= lst[1] and is_sorted(lst[1:])
print(is_sorted([])) # True
print(is_sorted([1])) # True
print(is_sorted([1, 2, 3, 4])) # True
print(is_sorted([1, 3, 2, 4])) # False재귀적으로 이진 탐색하기(다시 보기)
이진 탐색을 이 틀에 따라 재귀적으로 표현해 보겠습니다. 기저 조건: lo > hi이면 찾지 못한 것이므로 -1을 반환합니다. 신뢰: 올바른 절반에 대해 재귀 호출을 하면 대상 값을 찾거나 -1을 반환합니다. 구성: 중간 위치를 계산하고, 비교한 뒤, 해당하는 절반을 호출합니다. O(1) 공간을 사용하는 반복형이 운영 환경에서는 선호되지만, 재귀형은 분할 정복 구조를 명확하게 보여 줍니다.
def binary_search(arr, target, lo, hi):
if lo > hi: # base case: search space exhausted
return -1
mid = lo + (hi - lo) // 2
if arr[mid] == target:
return mid
# Trust both halves return correct results
if arr[mid] < target:
return binary_search(arr, target, mid + 1, hi)
else:
return binary_search(arr, target, lo, mid - 1)
arr = [1, 3, 5, 7, 9, 11]
print(binary_search(arr, 7, 0, len(arr) - 1)) # 3
print(binary_search(arr, 4, 0, len(arr) - 1)) # -1재귀와 반복 중 언제 사용할까
재귀는 문제가 같은 유형의 더 작은 하위 문제로 자연스럽게 분해될 때 뛰어납니다(트리, 분할 정복, 되추적). 다음과 같은 경우에는 반복이 선호됩니다. 재귀 깊이가 큰 경우(기본값이 약 1000인 Python에서 스택 오버플로 위험이 있음), 재귀형과 반복형이 똑같이 명확한 경우, 또는 문제가 단순한 반복문인 경우(팩토리얼, 메모이제이션을 사용하지 않는 피보나치).
경험에 따른 좋은 기준은 다음과 같습니다. 재귀 트리를 그리는 것이 자연스럽게 느껴지면 재귀를 사용하십시오. 트리가 직선이라면(꼬리 재귀) 반복으로 바꾸십시오.
import sys
# Python's default recursion limit
print('Recursion limit:', sys.getrecursionlimit()) # 1000
# A list of 2000 elements would overflow the recursive sum_list
# Use iteration for safety:
def sum_list_iter(lst):
total = 0
for x in lst:
total += x
return total
big = list(range(2000))
print(sum_list_iter(big)) # 1999000 — no stack overflow빠른 확인
이 단원에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인하십시오.
단원 복습
이 단원에서 배운 내용: 세 단계 틀은 기저 조건(가장 간단하고 알고 있는 답), 신뢰(하위 문제가 해결되었다고 가정), 구성(현재 원소와 신뢰한 결과를 결합)입니다, 기저 조건을 먼저 작성하고 전체 호출 트리를 머릿속으로 추적하지 마십시오, 재귀 깊이 때문에 스택 오버플로 위험이 있거나 재귀형과 반복형이 똑같이 명확할 때는 반복을 사용하십시오. 다음으로 호출 스택을 자세히 시각화합니다.
자주 묻는 질문
“재귀 프레임워크: 기본 사례, 신뢰, 구성” 강의는 무료인가요?
네 — “재귀 프레임워크: 기본 사례, 신뢰, 구성” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“재귀 프레임워크: 기본 사례, 신뢰, 구성”에서 뭘 배우나요?
모든 호출을 추적하지 않고 팩토리얼, 거듭제곱, 자릿수 합의 올바른 재귀 해법을 작성하는 세 단계 방법을 적용합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“재귀 프레임워크: 기본 사례, 신뢰, 구성” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 재귀 프레임워크: 기본 사례, 신뢰, 구성
- 호출 스택 시각화
- 재귀와 반복의 트레이드오프
- 메모이제이션: 재귀 결과 캐싱