Company
교육 철학

LV15 꼬리재귀 마스터 - 20문제 연습집 (Tail Recursion Practice)

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

꼬리재귀란?

재귀 호출이 함수의 마지막 연산인 재귀 → 스택 오버플로우 방지 + 최적화 가능
// ❌ 일반 재귀 (스택 쌓임) 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) 스택 공간
컴파일러 최적화: 반복문으로 자동 변환 가능
메모리 효율성: 깊은 재귀에서 안전

실전 코테 활용

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