Company
교육 철학

꼬리재귀 마스터 - 20문제 연습집

문제를 풀기전에 꼬리재귀 구현은 생각보다 그렇게 중요한 부분은 아니니 구현하는데 어려움이 느껴진다면 우리에게 편한 그냥 재귀로 풀어 주어도 좋습니다.

꼬리재귀란?

재귀 호출이 함수의 마지막 연산인 재귀 → 스택 오버플로우 방지 + 최적화 가능
// ❌ 일반 재귀 (스택 쌓임) int factorial(int n) { if (n <= 1) return 1; return n * factorial(n-1); // 곱셈 후 재귀 호출 } // ✅ 꼬리재귀 (스택 재사용 가능) int factorial_tail(int n, int acc = 1) { if (n <= 1) return acc; return factorial_tail(n-1, acc * n); // 재귀 호출이 마지막 }
C++
복사

1. 일반 재귀 vs. 꼬리 재귀 차이

일반 재귀
현재 함수가 다음 재귀 호출을 마친 뒤에도 추가 연산이 필요함.
예:
f(n) { result = f(n-1); // 재귀 호출 return result + 1; // 호출 이후에 연산이 있음 → 꼬리재귀 아님 }
C++
복사
꼬리 재귀
마지막 연산이 재귀 호출이며, 호출 이후 아무 일도 하지 않음.
예:
f(n, acc) { // acc: 누적값 if (n == 0) return acc; return f(n-1, acc+1); // 재귀가 마지막 동작 → 꼬리재귀 }
C++
복사

2. 현재 코드 분석

지금 run()은 아래처럼 재귀 호출 이후에도 일을 하고 있습니다.
path[level + 1] = value[i]; visited[i] = 1; run(i, level + 1); // 재귀 호출 path[level + 1] = 0; // ← 재귀 호출 후에 실행 → 꼬리재귀 불가능
C++
복사
즉, 백트래킹(Backtracking) 특성상 "재귀 후 원상복구"라는 작업이 있기 때문에
C++ 컴파일러가 꼬리 재귀 최적화(TCO)를 적용할 수 없습니다.

3. 꼬리 재귀로 바꾸는 방법

백트래킹을 쓰는 경우엔 꼬리 재귀로 바꾸기 어렵습니다.
대신 꼬리 재귀 구조로 바꾸려면 “스택 상태 복원”을 호출 전에 해결해야 합니다.
#include <iostream> char value[6] = "IEQWA"; char path[6] = ""; int visited[5] = {}; int map[5][5] = { 0,1,0,0,0, 0,0,1,1,0, 0,0,0,0,0, 1,0,0,0,1, 0,0,0,0,0, }; void run(int now, int level) { std::cout << value[now]; for (size_t i = 0; i < 5; i++) { if (map[now][i] == 1 && visited[i] == 0) { path[level + 1] = value[i]; visited[i] = 1; run(i, level + 1); path[level + 1] = 0; } } } int main() { path[0] = 'T'; visited[0] = 1; run(0, 0); return 0; }
C++
복사

아이디어 1 — 상태를 매개변수로 복사 전달

void run_tail(int now, int level, int visited[], char path[]) { std::cout << value[now]; for (size_t i = 0; i < 5; i++) { if (map[now][i] == 1 && visited[i] == 0) { int newVisited[5]; memcpy(newVisited, visited, sizeof(int)*5); char newPath[6]; memcpy(newPath, path, sizeof(char)*6); newPath[level + 1] = value[i]; newVisited[i] = 1; // 재귀가 마지막 실행 run_tail(i, level + 1, newVisited, newPath); } } }
C++
복사
장점: 꼬리 재귀 구조에 가깝게 변경 가능
단점: 매 호출마다 상태 복사 → 성능 손해

아이디어 2 — 반복문+스택으로 변환

꼬리 재귀의 장점은 사실 반복문으로도 완전히 대체 가능하므로,
아예 명시적으로 스택을 두고 구현하면 TCO 없이도 안전하게 동작합니다.
#include <stack> struct State { int now, level; int visited[5]; char path[6]; }; void run_iterative() { std::stack<State> st; State start{}; start.now = 0; start.level = 0; start.visited[0] = 1; start.path[0] = 'T'; st.push(start); while (!st.empty()) { State cur = st.top(); st.pop(); std::cout << value[cur.now]; for (size_t i = 0; i < 5; i++) { if (map[cur.now][i] == 1 && cur.visited[i] == 0) { State next = cur; next.level++; next.path[next.level] = value[i]; next.visited[i] = 1; st.push(next); } } } }
C++
복사

4. 팁

꼬리 재귀 최적화는 C++ 표준에 필수 조건이 아니므로,
컴파일러(-O2, -O3 등)와 아키텍처에 따라 적용 여부가 달라집니다.
백트래킹 알고리즘은 “재귀 이후 처리”가 거의 필수라, 통상적으로는
꼬리 재귀로 만들기보다 반복문+스택 변환이 더 안전합니다.
성능 목적으로 꼬리 재귀로 바꾸는 것보다, 명시적 스택 방식이 예측 가능성이 높습니다.

초급 문제 (1-7번)

1. 팩토리얼 (Factorial)

문제 설명: 팩토리얼은 수학에서 가장 기본적인 개념 중 하나입니다. n!은 1부터 n까지의 모든 자연수를 곱한 값을 의미합니다.
실생활 예시: 5명이 한 줄로 서는 경우의 수 = 5! = 120가지
첫 번째 자리: 5명 중 선택 (5가지)
두 번째 자리: 남은 4명 중 선택 (4가지)
세 번째 자리: 남은 3명 중 선택 (3가지)
네 번째 자리: 남은 2명 중 선택 (2가지)
다섯 번째 자리: 남은 1명 (1가지)
총 경우의 수: 5 × 4 × 3 × 2 × 1 = 120
계산 과정: 5! = 5 × 4 × 3 × 2 × 1 = 120
// 문제: n! 을 꼬리재귀로 구현하세요 int factorial_tail(int n, int acc = 1) { // TODO: 여기에 구현 } // 테스트: factorial_tail(5) = 120
C++
복사
정답 보기

2. 거듭제곱 (Power)

문제 설명: 거듭제곱은 같은 수를 여러 번 곱하는 연산입니다. base^exp는 base를 exp번 곱한다는 의미입니다.
실생활 예시:
컴퓨터 메모리: 2^10 = 1024 (1KB)
세포 분열: 1개 → 2개 → 4개 → 8개... (2의 거듭제곱)
복리 이자 계산
계산 과정: 2^10 = 2×2×2×2×2×2×2×2×2×2 = 1024
2^1 = 2
2^2 = 4
2^3 = 8
...
2^10 = 1024
// 문제: base^exp 를 꼬리재귀로 구현하세요 int power_tail(int base, int exp, int acc = 1) { // TODO: 여기에 구현 } // 테스트: power_tail(2, 10) = 1024
C++
복사
정답 보기

3. 피보나치 수열 (Fibonacci)

문제 설명: 피보나치 수열은 이탈리아 수학자 피보나치가 토끼 번식 문제를 통해 발견한 수열입니다. 각 수는 앞의 두 수를 더한 값입니다.
실생활 예시:
자연계의 나선 모양 (달팽이 껍질, 해바라기 씨앗 배열)
주식 시장 기술적 분석 (피보나치 되돌림)
황금비율과 관련
수열 전개: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...
F(0) = 0
F(1) = 1
F(2) = F(1) + F(0) = 1 + 0 = 1
F(3) = F(2) + F(1) = 1 + 1 = 2
F(4) = F(3) + F(2) = 2 + 1 = 3
F(5) = F(4) + F(3) = 3 + 2 = 5
// 문제: n번째 피보나치 수를 꼬리재귀로 구현하세요 int fibonacci_tail(int n, int a = 0, int b = 1) { // TODO: 여기에 구현 } // 테스트: fibonacci_tail(10) = 55
C++
복사
정답 보기

4. 1부터 N까지의 합 (Sum)

문제 설명: 연속된 자연수의 합을 구하는 가장 기본적인 문제입니다. 수학자 가우스가 어린 시절 1부터 100까지의 합을 빠르게 계산한 유명한 일화가 있습니다.
가우스의 공식: 1+2+...+n = n×(n+1)/2
실생활 예시:
계단식 좌석 배치 계산
삼각수 (점을 삼각형 모양으로 배열할 때 필요한 점의 개수)
누적 점수 계산
계산 과정: 1+2+3+4+5 = 15
단계별: 1 → 3 → 6 → 10 → 15
공식으로: 5×6/2 = 15
// 문제: 1+2+3+...+n 을 꼬리재귀로 구현하세요 int sum_tail(int n, int acc = 0) { // TODO: 여기에 구현 } // 테스트: sum_tail(100) = 5050
C++
복사
정답 보기

5. 최대공약수 (GCD - Greatest Common Divisor)

문제 설명: 최대공약수는 두 수의 공통된 약수 중 가장 큰 수입니다. 유클리드 호제법은 기원전 300년경 그리스 수학자 유클리드가 개발한 효율적인 알고리즘입니다.
유클리드 호제법 원리: 두 수 a, b에 대해 gcd(a, b) = gcd(b, a mod b) 나머지가 0이 될 때까지 반복하면 최대공약수를 구할 수 있습니다.
실생활 예시:
분수 약분 (48/18 → 8/3)
타일링 문제 (48cm × 18cm 공간을 정사각형 타일로 채울 때 가장 큰 타일 크기)
주기 계산
계산 과정: gcd(48, 18)
gcd(48, 18) → 48 = 18×2 + 12 → gcd(18, 12)
gcd(18, 12) → 18 = 12×1 + 6 → gcd(12, 6)
gcd(12, 6) → 12 = 6×2 + 0 → gcd(6, 0) = 6
// 문제: 유클리드 호제법을 꼬리재귀로 구현하세요 int gcd_tail(int a, int b) { // TODO: 여기에 구현 } // 테스트: gcd_tail(48, 18) = 6
C++
복사
정답 보기

6. 배열의 합 (Array Sum)

문제 설명: 배열에 저장된 모든 원소의 합을 구하는 기본적인 문제입니다. 데이터 분석의 가장 기초가 되는 연산입니다.
실생활 예시:
월별 매출 총합 계산
학생들의 총점 계산
쇼핑 카트 총 금액
센서 데이터의 평균값 계산을 위한 합계
처리 과정: {1, 2, 3, 4, 5}
초기값: sum = 0
1단계: sum = 0 + 1 = 1
2단계: sum = 1 + 2 = 3
3단계: sum = 3 + 3 = 6
4단계: sum = 6 + 4 = 10
5단계: sum = 10 + 5 = 15
// 문제: 배열의 모든 원소 합을 꼬리재귀로 구현하세요 int array_sum_tail(vector<int>& arr, int index = 0, int acc = 0) { // TODO: 여기에 구현 } // 테스트: {1,2,3,4,5} → 15
C++
복사
정답 보기

7. 배열의 최댓값 (Array Maximum)

문제 설명: 배열에서 가장 큰 값을 찾는 문제입니다. 정렬하지 않고 한 번의 순회로 최댓값을 찾는 효율적인 방법입니다.
실생활 예시:
최고 점수 찾기
최고 기온 찾기
최대 매출일 찾기
성능 측정에서 최대 처리량 찾기
탐색 과정: {3, 1, 4, 1, 5, 9, 2}
초기값: max = 3 (첫 번째 원소)
1 비교: max(3, 1) = 3
4 비교: max(3, 4) = 4
1 비교: max(4, 1) = 4
5 비교: max(4, 5) = 5
9 비교: max(5, 9) = 9
2 비교: max(9, 2) = 9
// 문제: 배열에서 최댓값을 꼬리재귀로 찾으세요 int array_max_tail(vector<int>& arr, int index = 0, int max_val = INT_MIN) { // TODO: 여기에 구현 } // 테스트: {3,1,4,1,5,9,2} → 9
C++
복사
정답 보기

중급 문제 (8-14번)

8. 문자열 뒤집기 (String Reverse)

문제 설명: 문자열의 문자 순서를 거꾸로 뒤집는 문제입니다. 양 끝에서부터 시작해서 가운데로 모여가며 문자를 교환합니다.
실생활 예시:
회문(팰린드롬) 검사의 전처리
문자열 암호화
데이터 검증
텍스트 처리 알고리즘
처리 과정: "hello" → "olleh"
초기: h-e-l-l-o (인덱스 0,1,2,3,4)
1단계: ho → o-e-l-l-h
2단계: el → o-l-l-e-h
3단계: 중간 l은 그대로 → o-l-l-e-h
// 문제: 문자열을 꼬리재귀로 뒤집으세요 string reverse_tail(string str, int start = 0, int end = -1) { if (end == -1) end = str.length() - 1; // 초기화 // TODO: 여기에 구현 } // 테스트: "hello" → "olleh"
C++
복사
정답 보기

9. 팰린드롬 검사 (Palindrome Check)

문제 설명: 팰린드롬(회문)은 앞에서 읽으나 뒤에서 읽으나 같은 문자열입니다. 대칭적인 구조를 가진 문자열을 판별하는 중요한 알고리즘입니다.
실생활 예시:
DNA 서열 분석 (회문 구조 탐지)
언어학 연구
암호화/복호화 검증
데이터 무결성 검사
팰린드롬 예시:
"racecar" (경주용 자동차)
"level" (단계)
"madam" (부인)
"noon" (정오)
"A man a plan a canal Panama" (공백 무시 시)
검사 과정: "racecar"
r과 r 비교 (양 끝) → 같음
a와 a 비교 → 같음
c와 c 비교 (중앙) → 같음
모두 같으므로 팰린드롬
// 문제: 문자열이 팰린드롬인지 꼬리재귀로 검사하세요 bool is_palindrome_tail(string str, int start = 0, int end = -1) { if (end == -1) end = str.length() - 1; // TODO: 여기에 구현 } // 테스트: "racecar" → true, "hello" → false
C++
복사
정답 보기

10. 이진수 변환 (Binary Conversion)

문제 설명: 10진수를 2진수로 변환하는 문제입니다. 컴퓨터는 모든 데이터를 0과 1로만 표현하므로 이진수 변환은 컴퓨터 과학의 기초입니다.
변환 원리: 10진수를 2로 나누면서 나머지를 기록하고, 나머지를 거꾸로 읽으면 2진수가 됩니다.
실생활 예시:
컴퓨터 메모리 주소 표현
네트워크 서브넷 마스크
비트 연산 최적화
디지털 신호 처리
변환 과정: 10 → "1010"
10 ÷ 2 = 5 나머지 0
5 ÷ 2 = 2 나머지 1
2 ÷ 2 = 1 나머지 0
1 ÷ 2 = 0 나머지 1
나머지를 역순으로: 1010
검증: 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 8 + 0 + 2 + 0 = 10 ✓
// 문제: 10진수를 이진수 문자열로 꼬리재귀 변환하세요 string to_binary_tail(int n, string result = "") { // TODO: 여기에 구현 } // 테스트: 10 → "1010"
C++
복사
정답 보기

11. 배열에서 특정 값 개수 세기 (Count Target)

문제 설명: 배열에서 특정 값이 몇 번 등장하는지 세는 문제입니다. 데이터 분석과 통계에서 빈도수를 계산하는 기본적인 연산입니다.
실생활 예시:
설문조사 결과 분석 (특정 답변 개수)
로그 분석 (에러 발생 횟수)
재고 관리 (특정 상품 개수)
성적 분포 분석 (특정 점수 빈도)
계산 과정: {1,2,3,2,2,4}에서 2의 개수
인덱스 0: 1 ≠ 2, count = 0
인덱스 1: 2 = 2, count = 1
인덱스 2: 3 ≠ 2, count = 1
인덱스 3: 2 = 2, count = 2
인덱스 4: 2 = 2, count = 3
인덱스 5: 4 ≠ 2, count = 3
// 문제: 배열에서 target 값의 개수를 꼬리재귀로 세어보세요 int count_target_tail(vector<int>& arr, int target, int index = 0, int count = 0) { // TODO: 여기에 구현 } // 테스트: {1,2,3,2,2,4}, target=2 → 3
C++
복사
정답 보기

12. 연결리스트 뒤집기 (Reverse Linked List)

문제 설명: 연결리스트의 링크 방향을 뒤집는 문제입니다. 각 노드의 next 포인터를 이전 노드를 가리키도록 변경합니다. 이는 자료구조의 핵심 조작 중 하나입니다.
연결리스트란: 데이터와 다음 노드의 주소를 가진 노드들이 연결된 선형 자료구조입니다.
실생활 예시:
웹 브라우저 뒤로가기 기능
음악 플레이리스트 역순 재생
실행 취소(Undo) 기능 구현
데이터베이스 링크드 인덱스 재구성
뒤집기 과정: 1→2→3을 3→2→1로
초기: 1→2→3→NULL
1단계: NULL←1 2→3→NULL
2단계: NULL←1←2 3→NULL
3단계: NULL←1←2←3
// 문제: 연결리스트를 꼬리재귀로 뒤집으세요 struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverse_list_tail(ListNode* head, ListNode* prev = nullptr) { // TODO: 여기에 구현 } // 테스트: 1→2→3 을 3→2→1 로
C++
복사
정답 보기

13. 이진 탐색 (Binary Search)

문제 설명: 정렬된 배열에서 특정 값을 효율적으로 찾는 알고리즘입니다. 중간값과 비교하여 탐색 범위를 절반씩 줄여나가는 분할정복 알고리즘입니다.
시간복잡도: O(log n) - 매우 효율적!
실생활 예시:
사전에서 단어 찾기
전화번호부에서 번호 찾기
데이터베이스 인덱스 검색
게임에서 숫자 맞추기
탐색 과정: {1,3,5,7,9}에서 5 찾기
범위: [0,4], 중간: 2, arr[2]=5 → 찾음!
만약 7을 찾는다면:
범위: [0,4], 중간: 2, arr[2]=5 < 7 → 오른쪽 탐색
범위: [3,4], 중간: 3, arr[3]=7 → 찾음!
// 문제: 정렬된 배열에서 target을 꼬리재귀로 찾으세요 int binary_search_tail(vector<int>& arr, int target, int left = 0, int right = -1) { if (right == -1) right = arr.size() - 1; // TODO: 여기에 구현 } // 테스트: {1,3,5,7,9}, target=5 → index 2
C++
복사
정답 보기

14. 퀵소트 분할 (QuickSort Partition)

문제 설명: 퀵소트 알고리즘의 핵심인 분할(Partition) 과정입니다. 피벗(기준값)을 선택하고, 피벗보다 작은 값들은 왼쪽에, 큰 값들은 오른쪽에 배치합니다.
퀵소트란: 분할정복을 이용한 평균 O(n log n)의 효율적인 정렬 알고리즘입니다.
실생활 예시:
학생들을 키 순으로 줄세우기
파일을 크기순으로 정렬
성적순 순위 매기기
데이터베이스 정렬 연산
분할 과정: {3,1,4,2,5}, 피벗=5
초기: [3,1,4,2,5]
3 < 5 → 왼쪽 그룹
1 < 5 → 왼쪽 그룹
4 < 5 → 왼쪽 그룹
2 < 5 → 왼쪽 그룹
결과: [3,1,4,2] + [5] + []
// 문제: 퀵소트의 분할을 꼬리재귀로 구현하세요 int partition_tail(vector<int>& arr, int low, int high, int i = -1, int j = -1) { if (i == -1) i = low - 1; // 초기화 if (j == -1) j = low; // 초기화 // TODO: 여기에 구현 (pivot = arr[high]) } // 테스트: 분할 후 피벗의 최종 위치 반환
C++
복사
정답 보기

고급 문제 (15-20번)

15. 이진트리 중위순회 (Binary Tree Inorder)

문제 설명: 이진트리를 특정 순서로 방문하는 순회 알고리즘입니다. 중위순회는 왼쪽 서브트리 → 루트 → 오른쪽 서브트리 순서로 방문합니다.
이진트리란: 각 노드가 최대 2개의 자식을 가지는 트리 자료구조입니다.
순회 방법:
전위순회: 루트 → 왼쪽 → 오른쪽
중위순회: 왼쪽 → 루트 → 오른쪽
후위순회: 왼쪽 → 오른쪽 → 루트
실생활 예시:
파일 시스템 탐색
수식 트리 계산 (중위순회하면 중위표기법)
이진 검색 트리에서 정렬된 순서로 출력
컴파일러 구문 분석 트리
// 문제: 이진트리를 꼬리재귀로 중위순회하세요 (스택 시뮬레이션) struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; void inorder_tail(TreeNode* root, vector<int>& result, stack<TreeNode*>& stk) { // TODO: 여기에 구현 }
C++
복사
정답 보기

16. 이진트리 높이 (Binary Tree Height)

문제 설명: 이진트리의 높이(또는 깊이)는 루트에서 가장 깊은 리프 노드까지의 경로 길이입니다. 트리의 균형도를 측정하는 중요한 지표입니다.
높이의 중요성:
검색 성능과 직결 (높이가 낮을수록 빠름)
트리 균형도 측정
메모리 사용량 예측
실생활 예시:
조직도의 계층 깊이
토너먼트 대진표의 라운드 수
파일 시스템의 폴더 깊이
의사결정 트리의 복잡도
높이 계산: 빈 트리는 높이 0, 단일 노드는 높이 1 각 노드에서 왼쪽과 오른쪽 서브트리의 높이 중 최댓값 + 1
// 문제: 이진트리의 높이를 꼬리재귀로 구하세요 int tree_height_tail(TreeNode* root) { // TODO: 여기에 구현 // 힌트: 서브트리들의 최대 높이를 비교 }
C++
복사
정답 보기

17. 미로 탈출 (Maze DFS)

문제 설명: 미로에서 시작점부터 목표점까지의 경로가 존재하는지 깊이우선탐색(DFS)으로 확인하는 문제입니다. 백트래킹을 사용하여 막다른 길에서 되돌아옵니다.
DFS (깊이우선탐색): 한 방향으로 끝까지 탐색한 후, 막히면 되돌아가서 다른 방향을 탐색하는 알고리즘
백트래킹: 조건에 맞지 않으면 이전 상태로 되돌아가는 기법
실생활 예시:
게임의 길찾기 AI
로봇의 경로 탐색
네트워크 연결성 확인
퍼즐 게임 솔버
탐색 과정:
1.
현재 위치 방문 표시
2.
목표점 도달 시 성공
3.
상하좌우 4방향 탐색
4.
막다른 길이면 방문 표시 해제 후 백트래킹
// 문제: 미로에서 출구까지의 경로를 꼬리재귀로 찾으세요 bool maze_dfs_tail(vector<vector<int>>& maze, int x, int y, int target_x, int target_y, vector<vector<bool>>& visited) { // TODO: 여기에 구현 // 0=길, 1=벽, 시작점에서 목표점까지 경로 존재 여부 }
C++
복사
정답 보기

18. 조합 생성 (Combinations nCr)

문제 설명: n개의 원소 중에서 r개를 순서 없이 선택하는 모든 경우의 수를 구하는 문제입니다. 조합론의 기본 개념으로 확률과 통계에서 광범위하게 사용됩니다.
조합 vs 순열:
조합: 순서 상관없음 (ABC = ACB = BAC)
순열: 순서 중요함 (ABC ≠ ACB ≠ BAC)
공식: C(n,r) = n! / (r!(n-r)!)
실생활 예시:
복권 번호 선택 (6개 중 4개)
팀 구성 (10명 중 5명 선발)
메뉴 조합 (여러 요리 중 몇 개 선택)
투자 포트폴리오 구성
생성 과정: {1,2,3,4}에서 2개 선택
{1,2}, {1,3}, {1,4} (1을 포함하는 조합)
{2,3}, {2,4} (1 제외, 2를 포함하는 조합)
{3,4} (1,2 제외, 3을 포함하는 조합)
// 문제: n개 중 r개를 선택하는 모든 조합을 꼬리재귀로 생성하세요 void combinations_tail(vector<int>& nums, int r, vector<int>& current, vector<vector<int>>& result, int start = 0) { // TODO: 여기에 구현 } // 테스트: {1,2,3,4}에서 2개 선택 → {{1,2},{1,3},{1,4},{2,3},{2,4},{3,4}}
C++
복사
정답 보기

19. 하노이의 탑 (Tower of Hanoi)

문제 설명: 세 개의 기둥과 크기가 다른 n개의 원판을 이용한 고전적인 퍼즐입니다. 큰 원판이 작은 원판 위에 올 수 없다는 규칙 하에 모든 원판을 다른 기둥으로 옮기는 문제입니다.
규칙:
1.
한 번에 한 개의 원판만 이동 가능
2.
큰 원판이 작은 원판 위에 올 수 없음
3.
보조 기둥 사용 가능
수학적 성질:
최소 이동 횟수: 2ⁿ - 1
3개 원판: 7번, 4개 원판: 15번
실생활 응용:
백업 시스템 설계
스케줄링 알고리즘
재귀적 사고 훈련
분할정복 알고리즘 이해
해결 전략:
1.
n-1개 원판을 보조 기둥으로 이동
2.
가장 큰 원판을 목표 기둥으로 이동
3.
n-1개 원판을 목표 기둥으로 이동
// 문제: 하노이의 탑을 꼬리재귀로 해결하세요 void hanoi_tail(int n, char from, char to, char aux, vector<string>& moves) { // TODO: 여기에 구현 } // 테스트: 3개 원판을 A에서 C로 이동 (B를 보조로 사용)
C++
복사
정답 보기

20. 메르센 소수 검사 (Mersenne Prime)

문제 설명: 메르센 소수는 2ᵖ - 1 형태의 소수입니다. 프랑스 수학자 마랭 메르센의 이름을 딴 특별한 형태의 소수로, 암호학과 컴퓨터 과학에서 중요한 역할을 합니다.
메르센 소수의 특징:
형태: 2ᵖ - 1 (p는 소수)
매우 큰 소수를 찾는 데 효율적
완전수와 밀접한 관련
알려진 메르센 소수:
M₂ = 2² - 1 = 3
M₃ = 2³ - 1 = 7
M₅ = 2⁵ - 1 = 31
M₇ = 2⁷ - 1 = 127
실생활 응용:
RSA 암호화 시스템
의사난수 생성기 (메르센 트위스터)
분산 컴퓨팅 프로젝트 (GIMPS)
수학적 연구
검사 방법: 2ᵖ - 1이 소수인지 확인하기 위해 √(2ᵖ-1)까지의 홀수로 나누어 확인
// 문제: 2^p - 1 형태의 메르센 소수를 꼬리재귀로 검사하세요 bool is_mersenne_prime_tail(int p, long long divisor = 3, long long mersenne = -1) { if (mersenne == -1) mersenne = (1LL << p) - 1; // 2^p - 1 // TODO: 여기에 구현 } // 테스트: p=5일 때 31은 소수인가? (2^5-1 = 31)
C++
복사
정답 보기

꼬리재귀 마스터 체크리스트

꼬리재귀 변환 패턴

1.
누적변수(Accumulator) 추가: 중간 결과를 매개변수로 전달
2.
재귀 호출이 마지막: return recursive_call(...)
3.
상태 정보 매개변수화: 지역 변수를 매개변수로 변환

성능 최적화 효과

스택 오버플로우 방지: O(n) → O(1) 스택 공간
컴파일러 최적화: 반복문으로 자동 변환 가능
메모리 효율성: 깊은 재귀에서 안전

실전 코테 활용

트리 순회: 스택 시뮬레이션으로 꼬리재귀화
백트래킹: 상태를 매개변수로 전달
분할정복: 구간 정보를 매개변수로 관리
꼬리재귀 = 반복문의 재귀 버전! 상태를 매개변수로 전달하는 것이 핵심입니다.