유클리드 알고리즘
두 수의 차/나머지를 반복해서 구하는 빠른 방법

- 핵심 아이디어: 두 수의 GCD는 그 차의 GCD와 같다, 또는 gcd(a,b)=gcd(b,amod b)로 계산
- 반복해서 나머지가 0이 될 때까지 진행하면 최종적으로 GCD가 나옴

피보나치 수열
1️⃣ 피보나치 수열의 정의: 앞의 두 항을 더해 다음 항을 만드는 수열

2️⃣ 재귀적 접근 (Recursive)

3️⃣ 반복적 접근 (Iterative)

왜 i가 3부터 시작하는가?
- int a = 1, b = 1, temp;
- a → F(1)
- b → F(2)
- 즉, 이미 첫 번째와 두 번째 항을 변수로 가지고 있는 상태예요.
- 이제 3번째 항부터 계산을 시작해야 합니다.
4️⃣ 메모이제이션 (Memoization, 개선된 재귀)

정리
| 방식 | 코드구조 | 시간복잡도 | 공간복잡도 | 특징 |
| 반복 (Iterative) | for문 | O(n) | O(1) | 가장 효율적, 간단 |
| 재귀 (Recursive) | 자기 호출 | O(2^n) | O(n) | 이해 쉬우나 매우 느림 |
| 메모이제이션 | 재귀 + 캐시 | O(n) | O(n) | 속도 빠르고 깔끔한 코드 |
주어진 정수 N이 소수인지 아닌지를 판별하는 방법
(1) 2부터 N-1까지 나눠보기
가장 단순한 방법이에요 👇
- 2부터 N-1까지의 모든 수로 N을 나눠서
나머지가 0인 수가 하나라도 있으면 → 소수가 아님 - 끝까지 없으면 → 소수
📘 예: N = 7
→ 2, 3, 4, 5, 6으로 나눠봄
→ 나누어떨어지는 수 없음 → ✅ 소수
➡️ 하지만 이 방법은 비효율적 (시간 복잡도 O(N))
(2) 2부터 N/2까지만 검사
조금 더 효율적으로!
- N의 절반보다 큰 수는 N을 나눌 수 없어요.
- 왜냐하면 어떤 수 A가 N을 나누려면, B = N / A 도 존재해야 하는데
만약 A가 N/2보다 크면 B는 2보다 작기 때문에 (1.x~) 정수가 안 됨.
📘 예: N=10
→ 2~5까지만 검사하면 됨.
6~9는 N/2보다 크니까 검사 불필요.
➡️ 계산 횟수가 절반으로 줄어요. (시간 복잡도 약 O(N/2))
(3) 2부터 √N까지만 검사
가장 효율적이고 정석적인 방법이에요 ⚡
왜 √N까지만 확인해도 될까?
- 만약 N이 소수가 아니라면,
N은 두 수의 곱으로 표현될 수 있음:N=a×bN = a \times b - 그런데 두 수 중 하나는 반드시 √N보다 작거나 같아요.
(예: 36 = 6 × 6, 또는 4 × 9 → 4는 √36=6보다 작음) - 즉, √N 이하의 수에서 이미 약수가 나오지 않으면
그 이상에서는 절대 약수가 안 나옵니다.
📘 예: N = 36
√N = 6
→ 2, 3, 4, 5, 6까지만 확인하면 충분해요.
→ 2로 나누어짐 → ❌ 소수 아님
에라토스테네스의 체 (Sieve of Eratosthenes)
소수를 찾는 효율적인 알고리즘.
소수가 아닌 수(합성수)를 ‘체로 걸러내듯이’ 지워 나가는 방식
🔹 기본 원리
- 2부터 N까지의 모든 수를 “남아 있는 후보”로 둠
- 2부터 시작해서, 아직 지워지지 않은 수를 소수로 확정
- 그 소수의 배수들을 전부 제거 (2×2, 2×3, 2×4, …)
- 다음으로 남은 수(지워지지 않은 첫 번째 수)를 소수로 확정
- 이 과정을 √N까지만 반복
(왜냐하면 √N보다 큰 수는 이미 작은 수의 배수로 걸러지기 때문)
🔹 예시: 53 이하의 수
- 처음엔 모든 수를 남겨둔다.
(2, 3, 4, 5, 6, 7, 8, 9, 10, ... , 53) - 소수 2 확정 → 2의 배수 제거 (4,6,8,10,...)
- 다음 소수 3 → 3의 배수 제거 (9,12,15,18,...)
- 다음 소수 5 → 5의 배수 제거 (25,30,35,...)
- √53 ≈ 7이므로 7까지만 검사
🔹 시간 복잡도
- 각 소수 p마다 배수를 제거하는 연산: 약 N/p번
- 전체 연산 횟수:

제곱 ㄴㄴ 수 (BOJ 1016)
“어떤 수 X가 1보다 큰 제곱수로 나누어 떨어지지 않을 때”
그 수를 제곱 ㄴㄴ 수(제곱 인수가 없는 수) 라고 해요.
예시:
| 10 | 없음 | ✅ |
| 12 | 4 (2²)로 나눠짐 | ❌ |
| 18 | 9 (3²)로 나눠짐 | ❌ |
| 30 | 없음 | ✅ |
문제 설명
- 주어진 구간 [min, max] 안에서
제곱 ㄴㄴ 수의 개수를 구하는 문제 - 예: [1, 10] → 제곱 ㄴㄴ 수 = 7개 (1, 2, 3, 5, 6, 7, 10)
접근 방법
- min~max 범위를 배열로 표시 (check[])
- 2², 3², 4², 5², … ≤ max 인 제곱수를 전부 구함
- 각 제곱수의 배수를 모두 check에서 제거 (true로 표시)
- 남은 수(=제곱수로 나누어지지 않은 수)를 카운트
예제
[min, max] = [11, 24]
1️⃣ 제곱수 후보: 2²=4, 3²=9, 4²=16
2️⃣ 각 제곱수의 배수 제거:
- 4의 배수: 12, 16, 20, 24
- 9의 배수: 18
- 16의 배수: 16 (이미 제거됨)
남는 수 → 11, 13, 14, 15, 17, 19, 21, 22, 23
✅ 9개
코드 구조 (C++)

| i * i <= max | 가능한 제곱수만 사용 |
| start_index = min / pow (+1) | 구간 내 첫 배수를 찾음 |
| check[j * pow - min] = true | 해당 수를 제곱수 배수로 표시 |
| 마지막에 check가 false인 수만 카운트 | ✅ 제곱 ㄴㄴ 수 |
시간 복잡도
- √max 이하의 제곱수만 탐색
- 각 제곱수의 배수 제거는 (max–min)/pow번 수행
- 전체적으로 O(√max) 정도로 빠름
(범위 [max–min] ≤ 1,000,000 조건 내에서 충분히 통과)
결론 요약
| 알고리즘 | 목적 | 핵심 | 복잡도 |
| 에라토스테네스의 체 | 소수 판별 | 배수 제거 방식 | O(N log log N) |
| 제곱 ㄴㄴ 수 | 제곱 인수 없는 수 개수 | 제곱수 배수 제거 | O(√N) |
재귀(Recursion)란?
“자기 자신을 호출하는 함수”
재귀는 큰 문제를 작은 문제로 쪼개서 푸는 방법
예를 들어 “1부터 n까지의 합”을 구하고 싶을 때:
- n = 5라면
→ 1 + 2 + 3 + 4 + 5
→ (1 + 2 + 3 + 4) + 5
→ 즉, “n-1까지의 합 + n” 형태로 반복됨
그래서 식으로는:
[sum(n) = n + sum(n-1)]
단, 끝나는 조건(종료 조건) 이 꼭 필요
그게 없으면 계속 자기 자신을 부르다가 “무한 호출 → Stack Overflow 오류”가 발생
예를 들어
if (n == 1)
return 1;
else
return n + sum(n-1);
이런 형태로 “언제 멈출지”를 명시해야 함
예시: 1부터 n까지의 합
반복문 버전
int sum(int n) {
int sum = 0;
for(int i = 1; i <= n; i++)
sum += i;
return sum;
}
👉 단순히 for문으로 반복하면서 더해감.
재귀 버전
int sum(int n) {
if (n == 1)
return 1; // 종료 조건
else
return n + sum(n - 1); // 자신을 호출
}
예를 들어 sum(3)을 계산하면:
sum(3)
= 3 + sum(2)
= 3 + (2 + sum(1))
= 3 + (2 + 1)
= 6
➡ 이렇게 함수가 자기 자신을 계속 호출하다가, n==1일 때 멈추고 되돌아가며 계산이 완료
n이 0이면?
n이 0일 때는 “아무 것도 더할 게 없다”는 뜻이니까
if (n <= 0)
return 0;
로 설정해두면 안전
예시: 팩토리얼 (Factorial)
팩토리얼은 “n! = n × (n-1) × (n-2) × … × 1”
반복문 버전
int factorial(int n) {
int fact = 1;
for(int i = 2; i <= n; i++)
fact *= i;
return fact;
}
재귀 버전
int factorial(int n) {
if (n == 0 || n == 1)
return 1;
else
return n * factorial(n - 1);
}
예:
factorial(3)
= 3 * factorial(2)
= 3 * (2 * factorial(1))
= 3 * (2 * 1)
= 6
재귀가 메모리에서 작동하는 방식
프로그램이 실행될 때 메모리는 프로세스 주소 공간으로 나뉨
| 구역 | 설명 |
| 코드(code) | 실행 명령어 저장 |
| 데이터(data) | 전역 변수, static 변수 |
| 힙(heap) | new, malloc으로 생성된 동적 메모리 |
| 스택(stack) | 함수 호출 시 임시 저장 공간 (매개변수, 지역 변수 등) |
재귀 함수는 스택을 계속 쌓음
예를 들어 sum(3)을 호출할 때:
| sum(3) | n=3 |
| sum(2) | n=2 |
| sum(1) | n=1 |
→ sum(1)에서 종료 조건을 만나면
스택이 하나씩 되돌아가며 (pop) 결과를 합침
그래서 재귀는 “메모리(스택)를 많이 사용한다”는 단점
반복문 vs 재귀의 메모리 효율
| 반복문 | 메모리 적게 사용 | 코드가 길어질 수 있음 |
| 재귀 | 코드 간결, 수학적 표현 쉬움 | 스택 많이 사용 (깊은 재귀 → StackOverflow 위험) |
재귀 함수 실행 예시 (스택 변화 시각화)
sum(3) 실행 시:
main()
└─ sum(3)
└─ sum(2)
└─ sum(1)
되돌아올 때:
sum(1) → 1 반환
sum(2) → 2 + 1 = 3 반환
sum(3) → 3 + 3 = 6 반환
배열 누적합 예시 (Java 버전)
public class SumMain {
static int sumA(int[] arr, int index){
if(index == 0)
return arr[index];
else{
arr[index] += sumA(arr, index - 1);
return arr[index];
}
}
public static void main(String[] args) {
int arr[] = {1,2,3,4,5,6};
int index = arr.length - 1;
sumA(arr, index);
for(int i=0; i<arr.length; i++)
System.out.print(arr[i] + " ");
}
}
실행 과정:
호출 순서 배열 상태
| sumA(arr, 5) | arr[5] += sumA(arr, 4) |
| sumA(arr, 4) | arr[4] += sumA(arr, 3) |
| … | |
| sumA(arr, 0) | 리턴 1 |
마지막 결과:
[1, 3, 6, 10, 15, 21]
즉, 각 원소가 앞의 모든 원소의 누적합으로 바뀜
'Algorithms' 카테고리의 다른 글
| Algorithms: 7장 알고리즘 정리 (0) | 2025.10.22 |
|---|---|
| Algorithms: 6장 알고리즘 정리 (0) | 2025.10.21 |
| Algorithms: 5장 알고리즘 정리 (0) | 2025.10.21 |
| Algorithms: 4장 알고리즘 정리 (0) | 2025.10.21 |
| Algorithms: 3장 알고리즘 정리 (0) | 2025.10.21 |