1. 소수 판별
여기서부터는 문법이 아니라 알고리즘입니다. 그런데 겁먹을 필요 없어요. 알고리즘의 8할은 "무식하게 다 해보기(완전탐색)"에서 시작해서 "똑똑하게 줄이기(최적화)"로 발전시키는 것이고, 소수 판별이 그걸 배우기에 가장 좋은 첫 문제입니다.
이 한 챕터에서 배우는 것:
- 완전탐색으로 문제를 먼저 푸는 법
- 시간복잡도(Big-O) 가 뭔지 감 잡기
- 같은 문제를 반복문 하나로 확 빠르게 만드는 법 (
√n최적화) - 여러 수를 한 번에 처리하는 에라토스테네스의 체
소수가 뭐였죠?
1과 자기 자신으로만 나누어지는, 1보다 큰 자연수. (2, 3, 5, 7, 11, 13, ...)
1은 소수가 아니고, 2는 유일한 짝수 소수입니다.
1단계: 무식하게 다 해보기 (완전탐색)
"n이 소수인가?"를 정의 그대로 옮기면 됩니다. 2부터 n-1까지 하나씩 나눠보고, 하나라도 딱 나누어떨어지면 소수가 아니다.
def is_prime(n):
if n < 2: # 1 이하는 소수가 아님
return False
for i in range(2, n): # 2, 3, ..., n-1 로 나눠보기
if n % i == 0: # 나누어떨어지면 = 약수를 찾았으면
return False # 소수가 아니다
return True # 끝까지 약수가 없었으면 소수다
print(is_prime(7)) # True
print(is_prime(12)) # False (2로 나누어짐)
print(is_prime(1)) # False
💡
return False가 반복문 안에서 나오는 순간 함수는 바로 끝납니다. 약수를 하나라도 찾으면 더 볼 필요가 없으니 즉시 탈출하는 거예요. 이게 반복문 최적화의 기본 감각입니다.
잠깐, 이게 얼마나 느릴까?
is_prime(n)은 최악의 경우 약 n번 반복합니다. n이 소수라면 끝까지 다 돌아야 하니까요.
n = 100→ 약 100번n = 1,000,000→ 약 100만 번n = 1,000,000,000→ 약 10억 번 😱
"입력이 커질수록 몇 번 일하나"를 대충 표현한 게 시간복잡도이고, 이걸 O(n) (오 엔) 이라고 씁니다. 위 코드는 O(n) 입니다.
💡 코딩테스트에서 "시간 초과"가 나면 대부분 이 시간복잡도가 너무 큰 겁니다. 보통 컴퓨터는 1초에 약 1억 번 연산합니다. 10억 번짜리 O(n)이면 시간 초과인 거죠.
2단계: 똑똑하게 줄이기 (√n 최적화)
핵심 관찰: 약수는 항상 짝을 지어 나온다.
36의 약수를 보면: 2 × 18, 3 × 12, 4 × 9, 6 × 6, 9 × 4, ...
6(=√36)을 기준으로 대칭입니다. 그래서 √n까지만 확인해서 약수가 없으면, 그 뒤로도 없습니다.
즉 2부터 √n까지만 나눠보면 됩니다.
def is_prime(n):
if n < 2:
return False
i = 2
while i * i <= n: # i * i <= n 는 i <= √n 과 같은 말 (제곱근 계산 없이!)
if n % i == 0:
return False
i += 1
return True
print(is_prime(97)) # True
print(is_prime(1000000007)) # True (이제 이 큰 수도 순식간에!)
i * i <= n은 i <= √n과 같습니다. 제곱근을 직접 구하면 실수 오차가 생길 수 있어서, 양변을 제곱한 i * i <= n 형태를 즐겨 씁니다.
이제 반복 횟수는 약 √n번. 이걸 O(√n) 이라고 합니다.
n = 1,000,000,000→ 약 √10억 ≈ 31,623번 (10억 번 → 3만 번으로!)
💡 같은 문제, 같은 답. 바뀐 건 반복 범위 하나뿐인데 성능이 3만 배 빨라졌습니다. "무식한 풀이 → 관찰 → 범위 줄이기" 이 흐름이 알고리즘 공부의 전부라고 해도 과언이 아닙니다.
3단계: 여러 수를 한 번에, 에라토스테네스의 체
"n이 소수인가?" 하나면 √n으로 충분합니다. 그런데 "1부터 N까지 소수를 전부 구하라" 면 이야기가 달라집니다. 하나하나 is_prime을 부르면 느려요.
발상을 뒤집습니다. 소수를 찾는 게 아니라, 소수가 아닌 것(배수)을 지워나갑니다.
- 2부터 N까지 다 소수 후보로 둔다.
- 2의 배수(4, 6, 8, ...)를 전부 지운다.
- 3의 배수(6, 9, 12, ...)를 전부 지운다.
- 다음 안 지워진 수(5)의 배수를 지운다... 반복.
- 끝까지 안 지워지고 살아남은 수가 소수다.
이걸 자료구조로 표현합니다. 여기서 리스트(불리언 배열) 가 핵심 도구예요.
def sieve(N):
# is_prime[i] = i가 소수인가? 일단 전부 True로 시작
is_prime = [True] * (N + 1)
is_prime[0] = is_prime[1] = False # 0과 1은 소수 아님
i = 2
while i * i <= N: # 여기서도 √N 까지만!
if is_prime[i]: # i가 아직 안 지워졌으면
for j in range(i * i, N + 1, i): # i*i부터 i씩 뛰며
is_prime[j] = False # i의 배수를 전부 지움
i += 1
# 살아남은 인덱스만 모으면 소수 목록
return [i for i in range(2, N + 1) if is_prime[i]]
print(sieve(30))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
두 가지 작은 최적화가 들어있어요:
- 바깥 반복도
i * i <= N까지만 (√N 최적화 재등장) - 지우기 시작점을
i * 2가 아니라i * i부터 (그 앞의 배수들은 더 작은 수가 이미 지웠으니까)
💡 리스트를 "값을 저장하는 상자"가 아니라 "체크 표시하는 칠판" 으로 쓰는 이 발상이 자료구조 활용의 시작입니다.
[True] * (N+1)로 인덱스를 곧 숫자 자체로 쓰는 패턴, 앞으로 아주 자주 만납니다.
집합(set)으로 빠른 소수 확인표 만들기
체로 구한 소수 목록을 집합에 넣으면, "이 수가 소수야?"를 즉시(O(1)) 확인할 수 있습니다.
primes = set(sieve(100)) # {2, 3, 5, 7, 11, ...}
print(41 in primes) # True
print(42 in primes) # False
리스트로 41 in primes를 하면 처음부터 훑지만(O(n)), 집합은 거의 즉시 찾습니다. "포함 여부를 자주 물어볼 때는 set" — 이것도 자료구조 감각입니다.
정리
| 접근 | 시간복잡도 | 언제 쓰나 |
|---|---|---|
완전탐색 (2 ~ n-1) |
O(n) | 개념 이해용. 실전엔 느림 |
√n 최적화 (2 ~ √n) |
O(√n) | 수 하나가 소수인지 판별 |
| 에라토스테네스의 체 | O(N log log N) | N까지 소수를 전부 구할 때 |
💡 문제를 만나면 ① 무식하게라도 일단 풀고 → ② 시간복잡도를 따져보고 → ③ 관찰로 범위를 줄이거나 자료구조를 바꾼다. 이 세 단계가 모든 알고리즘 문제에 통하는 사고 틀입니다. 소수는 그 첫 연습이었어요.
직접 풀어보기
is_prime(√n 버전)을 이용해 1부터 100까지 소수의 개수를 세보세요. (정답: 25개)sieve(N)로 1부터 1000까지 소수의 합을 구해보세요.- 두 소수의 합으로 나타낼 수 있는 짝수를 찾아보세요. (예:
10 = 3 + 7) — 체로 만든 소수 집합을 활용하면 쉽습니다.