Algorithm/Deep Dive

[Algorithm] 에라토스테네스의 체(Sieve of Eratosthenes)란?

dotz0ver 2025. 3. 5. 13:21

에라토스테네스의 체

소수(prime number)란?

1과 자기 자신을 제외한 다른 어떤 수로도 나누어지지 않는 자연수이다. 예를 들어, 2, 3, 5, 7, 11 등은 소수이다.

 

일반적으로 소수를 판별하는 방법 중 하나는 특정 수가 소수인지 하나씩 확인하는 O(√N) 방식이다. 예를 들어, 어떤 수 xx가 소수인지 판별하려면 1부터 xx까지 모든 수를 나누어보는 대신, √x까지만 확인하면 충분하므로 O(√N)의 시간 복잡도를 갖는다.

하지만 N까지의 모든 소수를 찾을 때 이 방식을 그대로 적용하면, 각 숫자마다 O(√N) 연산을 수행해야 하므로 전체적으로 O(N√N)의 시간 복잡도가 발생한다.
이 때문에 여러 개의 소수를 한 번에 찾기에는 비효율적이며, 보다 빠르게 소수를 구하기 위해 에라토스테네스의 체 같은 최적화된 알고리즘이 사용된다.

 

에라토스테네스의 체는 고대 그리스의 수학자 에라토스테네스(Eratosthenes) 가 고안한 소수(prime number)를 찾는 알고리즘이다. 이 방법은 2부터 N까지의 모든 소수를 빠르게 구하는 알고리즘으로, 특정 범위 내의 소수를 찾는 데 효과적이다.

 

알고리즘 원리

소수는 자신과 1을 제외한 다른 어떤 수로도 나누어지지 않는 수이므로, 어떤 수가 소수라면 그 수의 배수는 모두 소수가 아니다. 이를 기반으로 가장 작은 소수부터 시작하여 해당 수의 배수를 제거해 나가면 소수만 남게 된다.

 

에라토스테네스의 체는 다음과 같은 절차를 따른다:

  1. 2부터 원하는 범위(N)까지의 모든 수를 나열
  2. 가장 작은 수(2)부터 시작하여 해당 수의 배수를 지운다 (자기 자신은 제외)
  3. 남아있는 수 중에서 다음으로 작은 수를 선택하고 해당 수의 배수를 지운다
  4. 이를 반복하면 소수만 남게 된다

이 방법을 사용하면 모든 수에 대해 나눗셈 연산을 수행하는 대신 배수를 제거하는 방식으로 불필요한 연산을 줄일 수 있어 매우 효율적이다.

 

예제

초기 상태: 2부터 30까지의 수를 나열한다. (N = 30)

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

 

1. 2의 배수를 제거 (2는 소수이므로 남긴다)

2 3 - 5 - 7 - 9 - 11 - 13 - 15 - 17 - 19 - 21 - 23 - 25 - 27 - 29 -

 

2. 3의 배수를 제거

2 3 - 5 - 7 - - - 11 - 13 - - - 17 - 19 - - - 23 - - - 29 -

 

3. 5의 배수를 제거

2 3 - 5 - 7 - - - 11 - 13 - - - 17 - 19 - - - 23 - - - 29 -

 

남은 수: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 (소수)

 

구현

에라토스테네스의 체는 다양한 방식으로 구현할 수 있으며, 보통 사용하는 자료구조에 따라 성능과 메모리 사용량이 달라진다. 대표적으로 다음과 같은 방식들이 있다.

구현 방식 시간 복잡도 공간 복잡도 특징
리스트(List) 활용 O(N * log(log N)) O(N) 가장 기본적인 방식, 직관적이고 빠름
집합(Set) 활용 O(N * log(log N)) O(N) 집합 연산 활용으로 가독성이 좋지만, 리스트보다 속도가 느릴 수 있음
비트마스크(Bitmask) 활용 O(N * log(log N)) O(N / 8) 메모리 사용량이 적으며 대규모 데이터 처리에 유리함

 

에라토스테네스의 체의 시간 복잡도는 O(N log log N) 으로 표현되는데, 이는 다음과 같이 유도된다:

  • 각 소수 에 대해, 그 배수들을 지우는 작업이 이루어진다.
  • 어떤 수 가 지워지는 횟수는, 해당 수의 최소 소인수를 기준으로 한다.
  • 즉, 한 수는 여러 번 지워질 필요 없이 한 번만 지워진다.
  • 전체적으로 소수의 개수에 비례한 연산이 수행되며, 이는 N / log N 개 정도이다.
  • 따라서, 최종적으로 O(N log log N) 으로 표현된다.

이는 일반적인 소수 판별 방법인 O(N√N) 보다 훨씬 빠른 성능을 제공한다.

 

또한, 에라토스테네스의 체의 공간 복잡도는 O(N) 이며, 이는 소수 여부를 저장하기 위해 사용되는 리스트 또는 비트 배열이 N 크기의 메모리를 차지하기 때문이다.

 

코드 1 : 리스트

가장 일반적으로 사용되는 방법으로, 불리언 배열을 사용해 소수를 판별한다. 구현이 직관적이고 빠르며, 추가적인 라이브러리가 필요하지 않아 널리 사용된다.

def sieve_of_eratosthenes(n):
    is_prime = [True] * (n + 1)
    is_prime[0], is_prime[1] = False, False
    
    for i in range(2, int(n**0.5) + 1):
        if is_prime[i]:
            for j in range(i * i, n + 1, i):
                is_prime[j] = False
    
    return [i for i in range(2, n + 1) if is_prime[i]]

 

코드 2 : 집합 자료구조

배열 대신 집합(set)을 활용하여 배수를 제거하는 방식이다. 코드가 간결하지만, 내부적으로 해시 연산이 발생하여 리스트보다 연산 비용이 증가할 수 있다.

def sieve_with_set(n):
    primes = set(range(2, n + 1))
    
    for i in range(2, int(n**0.5) + 1):
        if i in primes:
            primes -= set(range(i * i, n + 1, i))
    
    return sorted(primes)

 

코드 3 : 비트마스크

비트 배열을 사용하여 메모리 사용량을 줄이는 방식이다. 리스트 대비 메모리를 1/8 수준으로 절약할 수 있지만, 비트 연산이 필요하며 bitarray 같은 라이브러리를 사용해야 한다.

from bitarray import bitarray

def sieve_with_bitmask(n):
    is_prime = bitarray(n + 1)
    is_prime.setall(True)
    is_prime[0], is_prime[1] = False, False
    
    for i in range(2, int(n**0.5) + 1):
        if is_prime[i]:
            is_prime[i * i : n + 1 : i] = False
    
    return [i for i in range(2, n + 1) if is_prime[i]]