1부터 입력받은 숫자 n 사이에 있는 소수의 개수를 반환하는 함수, solution을 만들어 보세요.
소수는 1과 자기 자신으로만 나누어지는 수를 의미합니다. (1은 소수가 아닙니다.)
- n은 2이상 1000000이하의 자연수입니다.
const solution = (n) => {
let count = 0;
for (let i = 2; i <= n; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
};- 소수 판별에서 가장 일반적인 방법은 1부터 그 수의 제곱근까지 1씩 증가시키며 그 수를 나누어보는 것이다. 만약 어떤 수로도 나누어지지 않는다면 그 수는 소수다.
- 1부터
n까지 판별을 반복하며 만약 어떤 수가 소수로 판별되면 카운트를 1씩 올린다.
- 소수 개수를 셀
count변수를0으로 초기화한다. - 2부터
n까지 for문을 돌려 범위 내의 모든 수를 검사한다.- 1은 소수가 아니므로 범위에서 제외한다.
isPrime을true로 초기화한다.- 2부터 현재 소수 여부를 판별하고자 하는 값의 양의 제곱근까지 1씩 증가시키며 현재 수를 나눠본다.
- 만약 하나라도 나누어떨어지게 만드는 수가 범위 안에 존재한다면
isPrime에false를 할당하고 내부 반복문을 탈출한다.
- 만약 하나라도 나누어떨어지게 만드는 수가 범위 안에 존재한다면
- 내부 반복문이 끝난 후 만약
isPrime이true라면(범위 내에서 현재 수를 나누어떨어지게 만드는 수가 존재하지 않았다면)count를 1 증가시킨다. - 범위 내의 모든 수에 대해 같은 작업이 끝나면 최종적으로
count를 반환한다.
- 효율성 테스트를 통과하긴 했지만 이중반복문으로 인해 시간복잡도가 증가한다는 점, 특히 불필요한 검사가 계속되는 것(어떤 소수를 발견했다면 그 소수의 배수들은 자동으로 소수 목록에서 제외되어야 한다.)이 마음에 걸렸다.
- 또한 소수 판별은 오래전부터 존재했던 문제이므로 소수인지의 여부를 판별할 수 있는 보다 효율적인 알고리즘이 분명 존재할 것이라고 생각했다.
const solution = (n) => {
let primeCount = 0;
let numbers = [];
for (let i = 2; i <= n; i++) numbers.push(i);
while (numbers.length > 0) {
const primeNumber = numbers.shift();
primeCount++;
if (primeNumber ** 2 > n) continue;
numbers = numbers.filter((number) => number % primeNumber !== 0);
}
return primeCount;
};- '에라토스테네스의 체' 알고리즘을 사용한다.
- 자연수 목록에서 가장 작은 수를 소수라고 가정한다(1은 제외).
- 목록 안에서 그 수를 제외한 해당 수의 배수를 모두 제거한다.
- 다음으로 작은 수에 대해 같은 방법을 적용한다.
- 다음으로 작은 수가 n의 제곱근보다 크면 종료한다(n의 제곱근보다 큰 수의 배수는 범위를 초과하므로.).
- 목록에 남아있는 수가 해당 범위에 존재하는 소수이다.
- 소수 개수를 카운팅 할
primeNumber를 0으로 초기화한다. numbers배열에 2부터n까지의 자연수를 담아 초기화한다.numbers의 길이가 0이 될 때까지 밑의 처리를 반복한다.numbers에서 가장 작은 수를 제거하여primeNumber에 저장한다.- 소수 카운트를 1 올린다.
- 만약
primeNumber의 제곱이n보다 크면 다음 반복으로 넘어간다. - 아니라면
numbers에filter()메소드를 사용하여primeNumber의 배수를 모두 제거한다.
- 해당 작업이 모두 끝나면 소수 카운트를 반환한다.
- 효율성 테스트에서 탈락했다.
filter메소드를 사용하여 완전탐색하는 과정에서 소수의 배수를 미리 제외하는 장점을 잃어버렸다.- 어떤 수의 배수는 미리 계산할 수 있으므로 완전탐색할 필요가 없기 때문이다.
const solution = (n) => {
const isPrimeMap = new Array(n + 1).fill(true);
for (let i = 2; i <= n ** 0.5; i++) {
if (isPrimeMap[i]) {
for (let j = i * 2; j <= n; j += i) isPrimeMap[j] = false;
}
}
let cnt = 0;
for (let i = 2; i <= n; i++) {
if (isPrimeMap[i]) cnt++;
}
return cnt;
};Array객체 생성자를 사용하여n + 1개의 요소를true로 초기화한다.
- 배열의 인덱스를 소수 판별 대상으로 사용할 것이기 때문에
n + 1개의 요소를 만든 후, 인덱스 1부터n까지 사용한다.
- 2부터
n의 제곱근 이하까지 반복문을 돌린다.
- 만약 해당 인덱스의 값이
true라면 해당 수는 소수로 간주하고 해당 수의 배수를 모두 제거한다.- 해당 수의 2배수부터 시작하여
n이하의 배수에 해당하는 인덱스까지를 모두false로 바꾼다.
- 해당 수의 2배수부터 시작하여
- 판별 작업이 끝난 후, 값이
true인 요소의 개수를 센다. - 최종적으로 센 값을 반환한다.
- 특정 수가 소수인지 판별할 떄는 2부터 해당 수의 제곱근까지 나눠보기, 특정 범위에서 소수를 찾아낼 때는 에라토스테네스의 체 쓰기.
- 이 두 방법은 공식처럼 외우고 있어도 나쁘지 않을 것 같다.