ryokatsu.dev

エラトステネスのふるい


エラトステネスのふるいを実装してみました。

エラトステネスのふるいって何?

エラトステネスのふるいは、ある数から素数を見つけるためのアルゴリズムである。 0と1を最初に除外したのちに、小さい順にその数と倍数を順番に除外していき、Nの平方根以下のすべての素数まで繰り返し、残った数が素数である。

以下が分かりやすい

実装

最初に実装したのは以下


const eratosthenes = (value: number): number[] => {
  // 調べたい数が2未満の場合
  if (value < 2) return [];

  // 調べたい数が2だった場合
  if (value === 2) return [2];

  // 調べたい数の最大値までの整数を生成する
  const primesData = [...Array(value - 1).keys()].map(i => i + 2);

  /**
   * 素数の倍数を除外する関数
   */
  const removePrimeMultiples = (candidates: number[], currentPrime: number): number[] => 
    candidates.filter(num => num === currentPrime || num % currentPrime !== 0);

  /**
   * 再帰的に素数を見つける関数
   */
  const sievePrimes = (candidates: number[], currentIndex = 0): number[] => {
    const currentPrime = candidates[currentIndex];
    if (currentIndex >= candidates.length || currentPrime * currentPrime > value) {
      return candidates;
    }
    
    // 現在の素数の倍数を除外して次の素数を探す
    const filteredCandidates = removePrimeMultiples(candidates, currentPrime);
    return sievePrimes(filteredCandidates, currentIndex + 1);
  };
  
  return sievePrimes(primesData);
};

console.log(eratosthenes(100));

0と1は除外なので、早期リターンをした。

しかしそもそも型レベルで除外すれば良さそうと思い、Branded Typesを使って実装してみた。

/**
 * 有効なエラトステネス入力値の型(2以上の整数)
 */
type EratosthenesInput = number & { readonly __brand: 'EratosthenesInput' };

/**
 * 2以上の数値であることを検証する
 */
const asValidNumber = (value: number): EratosthenesInput => {
  if (value < 2) {
    throw new Error('エラトステネスのふるいは2以上の数値でのみ使用できます。');
  }
  return value as EratosthenesInput;
};

/**
 * エラトステネスのふるいを使用して素数を見つける関数
 */
const eratosthenes = (value: EratosthenesInput): number[] => {
  // 2の場合は特殊処理
  if (value === 2) {
    return [2];
  }
  
  // 調べたい数の最大値までの整数を生成する
  const candidates = [...Array(value - 1).keys()].map(i => i + 2);
  
  /**
   * 素数の倍数を除外する関数
   */
  const removePrimeMultiples = (candidates: number[], prime: number): number[] => 
    candidates.filter(candidate => candidate === prime || num % prime !== 0);
  
  /**
   * 素数を篩い落とす再帰関数
   */
  const sieve = (candidates: number[], index = 0): number[] => {
    const prime = candidates[index];
    if (index >= candidates.length || prime * prime > value) {
      return candidates;
    }
    // 現在の素数の倍数を除外して次の素数を探す
    return sieve(removePrimeMultiples(candidates, prime), index + 1);
  };
  
  return sieve(candidates);

  console.log(eratosthenes(asValidNumber(100)));
};

これで良いのかは微妙。eratosthenes関数に0や1を渡すときに型エラーで止まるシンプルなものだと

type NumberGreaterThanTwo<T extends number> = T extends 0 | 1 ? never : T;

みたいなこともできるかもしれない。どちらが良いんだろうか。Brand Typeだと安全性は高いかもしれないが、今回のケースだとやり過ぎだったりするかもしれない。

AIくんに聴いてみると、実行時の検証が明示的だったり拡張性が増す一方で、実行時のオーバーヘッドがあるとのことだった。

おわりに

Branded Typesの理解をもっと深めたい。