문제를 풀기전에 꼬리재귀 구현은 생각보다 그렇게 중요한 부분은 아니니 구현하는데 어려움이 느껴진다면 우리에게 편한 그냥 재귀로 풀어 주어도 좋습니다.
꼬리재귀란?
재귀 호출이 함수의 마지막 연산인 재귀 → 스택 오버플로우 방지 + 최적화 가능
// ❌ 일반 재귀 (스택 쌓임)
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단계: h
o → o-e-l-l-h
•
2단계: e
l → 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) 스택 공간
•
컴파일러 최적화: 반복문으로 자동 변환 가능
•
메모리 효율성: 깊은 재귀에서 안전
실전 코테 활용
•
트리 순회: 스택 시뮬레이션으로 꼬리재귀화
•
백트래킹: 상태를 매개변수로 전달
•
분할정복: 구간 정보를 매개변수로 관리
꼬리재귀 = 반복문의 재귀 버전! 상태를 매개변수로 전달하는 것이 핵심입니다. 