质数打表的四种方式

Make Charts

  • 朴素打表
  • 朴素改进
  • Eratosthenes 筛法
  • 欧拉筛法


不多聊,开始。


朴素打表


  一种朴素的想法,就是把每个数对它可能的因数取余,判断是否不存在能将其分解的数,并将其记录在表中。

    public boolean[] makeCharts(int n) {
     
        boolean[] charts = new boolean[n + 1];
        for (int i = 2; i <= n; i++) {
     
            boolean flag = false;
            for (int k = 2; k * k <= i; k++)
                if (flag = i % k == 0)
                    break;
            charts[i] = !flag;
        }
        return charts;
    }

  这里解释一下k * k <= i这个点。

  如果存在大于 i \sqrt{i} i 的数 k 1 k_{1} k1 能作为 i i i 的因数,这里写作 i ÷ k 1 = k 2 i ÷ k_{1} = k_{2} i÷k1=k2,即 k 1 k 2 = i k_{1}k_{2} = i k1k2=i,我们将两遍同时除以一个 i \sqrt{i} i ,得   k 1 i   k 2 = i \cfrac{\ k_{1}}{\sqrt{i}}\ k_{2} = \sqrt{i} i  k1 k2=i ,由于 k 1 > i k_{1} > \sqrt{i} k1>i ,所以   k 1 i > 1 \cfrac{\ k_{1}}{\sqrt{i}} > 1 i  k1>1,故 k 2 < i k_{2} < \sqrt{i} k2<i ,所以与 k 2 k_{2} k2 在顺序枚举时,会在 i \sqrt{i} i 之前出现,因此这种写法的正确性有了保障。

  整个算法的时间复杂度为 O ( n 3 ) O(\sqrt{n^{3}}) O(n3 )


朴素改进


  把这里做的改进单独列为一种的原因是,

  我们打出一个质数表,场景通常是高频的判断,或者使用,使用布尔数组为区间内的质数打上标记固然使得判断速度大幅提升,但同时我们要使用一个范围内的质数时,似乎没有什么高效的方式可以实现。

  在高频使用的场景,我们可以对朴素打表的方法做出一点小小的更改,把打上标记这一操作更变为将质数加入一个序列,但这里要给出的不是这种简单的变更,而是基于操作变更后的一定小改进。

    public List<Integer> makeCharts(int n) {
     
        List<Integer> charts = new ArrayList();
        if (n >= 2) charts.add(2);
        for (int i = 3; i <= n; i++) {
     
            boolean flag = false;
            for (int k : charts)
                if (k * k > i || (flag = i % k == 0)) break;
            if (!flag) charts.add(i);
        }
        return charts;
    }

  在朴素打表中,我们知道只需要枚举 i \sqrt{i} i 内的所有正整数,就能判断一个数是否为质数,而基本算术定理又能告诉我们,每个大于 1 1 1 的自然数都能分解成若干质数的幂的乘积,而在整个打表过程中,我们又能快速的遍历小于 i i i 的质数的集合,总和起来就是这个改进的全貌。

  这里使用 n ln ⁡ n \cfrac{n}{\ln n} lnnn 近似质数分布,该算法时间复杂度为 O ( n 3 log ⁡ n ) O(\cfrac{\sqrt{n^{3}}}{\log \sqrt{n}}) O(logn n3 )


Eratosthenes 筛法


  又称埃氏筛、爱氏筛、质数筛、普通筛法,是较为简单的质数筛法之一。

  一个大于 1 1 1 的自然数要么是质数,要么是合数,如果我们将 n n n 以内的自然数集中,所有小于 n \sqrt{n} n 的质数的倍数全部筛选出来,剩下数的数集就是我们要的质数表。

    public boolean[] makeCharts(int n) {
     
        boolean[] charts = new boolean[n + 1];
        for (int i = 2; i <= n; i++)
            charts[i] = true;
        for (int i = 2; i <= n; i++)
            if (charts[i])
                for (int k = i << 1; k <= n; k += i)
                    charts[k] = false;
        return charts;
    }

  复杂度可以简单的考虑如下:

  内层循环执行 n p − 1 \cfrac{n}{p} - 1 pn1 p p p 为质数。

  时间复杂度为 O ( ∑ p   ≤ n n p − 1 ) O(\displaystyle\sum_{p\ \leq\sqrt{n}}\cfrac{n}{p} - 1) O(p n pn1),即 O ( n ∑ p   ≤ n 1 p − 1 n ) O(n\displaystyle\sum_{p\ \leq\sqrt{n}}\cfrac{1}{p} - \cfrac{1}{n}) O(np n p1n1)

  为了方便计算,引入质数分布定理, π ( x ) \pi(x) π(x) 为小于 x x x 的质数个数, π ( x ) ≈ x ln ⁡ x \pi(x)\approx\frac{x}{\ln x} π(x)lnxx,第 i i i 个质数约为 i ln ⁡ i i \ln i ilni

   原 式 = O ( n I ) 原式=O(nI) =O(nI) I = 1 2 + ∑ i = 2 n ln ⁡ n ( 1 i ln ⁡ i − 1 n ) I = \cfrac{1}{2} +\displaystyle\sum_{i = 2}^{\frac{n}{\ln n}}(\cfrac{1}{i \ln i} - \cfrac{1}{n}) I=21+i=2lnnn(ilni1n1),这里把第一个质数拿了出来。

  积分估计一下 I I I 的值, f ( x ) = 1 x ln ⁡ x f(x) = \cfrac{1}{x \ln x} f(x)=xlnx1,求 ∫ 2 n ln ⁡ n f ( i )   d i − ∫ 2 n ln ⁡ n 1 n + 1 2 \displaystyle\int_{2}^{\frac{n}{\ln n}}f(i)\, di-\displaystyle\int_{2}^{\frac{n}{\ln n}}\cfrac{1}{n} + \cfrac{1}{2} 2lnnnf(i)di2lnnnn1+21

  上牛茨公式, F ( x ) = ln ⁡ ln ⁡ x F(x) = \ln \ln x F(x)=lnlnx I = F ( n ln ⁡ n ) − F ( 2 ) − ∫ 2 n ln ⁡ n 1 n + 1 2 = ln ⁡ ln ⁡ n − ln ⁡ ln ⁡ ln ⁡ n − ln ⁡ ln ⁡ 2 − n − 2 ln ⁡ n n ln ⁡ n + 1 2 I = F(\frac{n}{\ln n}) - F(2) -\displaystyle\int_{2}^{\frac{n}{\ln n}}\cfrac{1}{n} + \cfrac{1}{2} = \ln \ln n - \ln \ln \ln n - \ln \ln 2- \cfrac{n - 2 \ln n}{n \ln n}+ \cfrac{1}{2} I=F(lnnn)F(2)2lnnnn1+21=lnlnnlnlnlnnlnln2nlnnn2lnn+21

  综上,埃氏筛时间复杂度为 O ( n log ⁡ log ⁡ n ) O(n\log \log n) O(nloglogn)


欧拉筛法


  同样是筛出合数,欧拉筛法的策略相较于埃氏筛有很大不同。

  当遍历到任意数 n i n_{i} ni 时,我们将所有 p j n i p_jn_i pjni p j ∈ p r i m e s p_j \in primes pjprimes 筛掉,

  首先要讨论的是这一步的完全性,对于将要访问的合数 n k n_{k} nk,我们可以将其表示为 p j m k p_{j}m_{k} pjmk,其中 p j p_{j} pj n k n_{k} nk 的最小质因数,这是因为,对于一个合数,我们总是能表示成一个最小因数和另一个数的乘积,而最小因数总是一个质数。

  因此 p j ≤ m k p_{j} \leq m_{k} pjmk,而我们在遍历到 m k m_{k} mk 时, p j p_{j} pj 已经加入了 p r i m e s primes primes 集合,故这种策略是完全的。

  其次要讨论的是在 n i m o d       p j = 0 n_i\mod \ \ p_j = 0 nimod  pj=0 时不再继续筛选的正确性,当 n i n_i ni p j p_j pj 的整数倍时,我们记 m i m_i mi m i = n i ÷ p j m_i = n_i ÷ p_j mi=ni÷pj,对于新的合数 n k n_k nk 我们可以记为 n k = n i × p j + 1 = m i × p j × p j + 1 n_k = n_i × p_{j+1}=m_i × p_j × p_{j+1} nk=ni×pj+1=mi×pj×pj+1,这说明 n i × p j + 1 n_i × p_{j+1} ni×pj+1 p j p_j pj 的整数倍,现在不继续筛选,遍历到 n k n_k nk 前也会被 m i × p j + 1 m_i × p_{j+1} mi×pj+1 继续筛选。

    public List<Integer> makeCharts(int n) {
     
        List<Integer> charts = new ArrayList();
        boolean[] marked = new boolean[n + 1];
        for (int i = 2; i <= n; i++) {
     
            if (!marked[i]) charts.add(i);
            for (int p : charts) {
     
                if (i * p > n) break;
                marked[i * p] = true;
                if (i % p == 0)break;
            }
        }
        return charts;
    }

每个合数只被筛选一次,故算法时间复杂度为 O ( n ) O(n) O(n)

也因此,欧拉筛也被称为线性筛。

你可能感兴趣的:(算法随笔,java,数学)