质数判断——暴力方法、埃氏筛与线性筛

质数判断

  问题背景为:我们希望判断前n个数是否为质数,即得到isPrime[n+1](此处沿用java语言定义)数组。

暴力方法

  传统意义上的对质数的判断方法,是依据质数的定义——质数不能被大于1小于自身的数整除。所以本质上就是对所有小于它的数进行遍历整除,如果存在能够整除的则说明不是质数,否则为质数。很明显时间复杂度为O(n)
  基于此方法,我们可以进行一定的优化,如根据定理:合数一定有一个因子小于\(\sqrt{n}\),来使遍历范围由[2, n-1]转为[2, floor(\(\sqrt{n-1}\))],时间复杂度为O(\(\sqrt{n}\))。

private static boolean[] getPrimeArray__ByRude(int toNum) {
      boolean[] isPrime = new boolean[toNum+1];
      for (int i = 2;i <= toNum;i++) {
          isPrime[i] = true;
      }

      for (int i = 2;i <= toNum;i++) {
          int sqrt = (int) Math.floor(Math.sqrt(i));
          for (int j = 2;j <= sqrt;j++) {
              if (i % j == 0) {
                  isPrime[i] = false;
                  break;
              }
          }
      }
      return isPrime;
  }

  很明显,上述算法时间复杂度为O(n\(\sqrt{n}\))。

埃氏筛

  暴力方法只是将单个数是否为质数得判断方法对区间内所有数进行使用,没有考虑到数与数相互之间的联系,所以无论如何对传统方法优化都无法在时间复杂度上进行进一步突破。
  现在考虑:对于一个质数p,当我们把它当作因子时,对任意的i>=2,i*p一定是合数。
  因此,我们可以通过上述思想从2到n找到所有的合数,进而也就是找到了所有的质数。
  其中有一个可以优化的点:对于质数p,事实上我们只需要考虑i>=p部分,也就是i作为i*p的因子;这是因为对于i小于p的情况,i(或者i的因子)已作为i*p的因子来标记了i*p是合数。

private static boolean[] getPrimeArray__ByEratosthenesSieve(int toNum) {
      boolean[] isPrime = new boolean[toNum+1];
      for (int i = 2;i <= toNum;i++) {
          isPrime[i] = true;
      }

      for (int i = 2;i <= toNum;i++) {
          if (isPrime[i]) {
              for (int j = i*i; j <= toNum;j+=i) {
                  isPrime[j] = false;
              }
          }
      }
      return isPrime;
  }

  不加证明的,时间复杂度为O(nloglogn)。

线性筛

  对于埃氏筛,虽然没有证明时间复杂度,但是我们思考为什么其时间复杂度中会多出loglogn这部分。
  埃氏筛事实上是通过对所有合数进行标记的方式进行判断的,理论上如果所有合数仅标记一次,那么时间复杂度应该为O(n)。但事实上多出一部分,是因为有些合数被多次标记。例如:45被3和5同时标记
  所以线性筛的核心目的就是避免多次标记
  与埃氏筛不同,线性筛通过一个primes数组记录了从小到大出现的质数,并且对于从2开始的数i,将i与所有此时已经记录在primes中的质数相乘,并将其标记为合数。事实上,所对于i,如果在遍历到它之前没有被标记为合数,那么将其视为素数。
  线性筛的核心点在于:如果i可以被一个primes中的p整除,即i%p==0,那么对于所以之后的p都不再考虑(注意,与p的乘积需要标记合数)。这是因为此后的合数i*p会i/p*p_标记(p_为p的下一个质数),这保证了所有合数被其最小质因数标记。

boolean[] isPrime = new boolean[toNum+1];
      ArrayList<Integer> primes = new ArrayList<>();
      for (int i = 2;i <= toNum;i++) {
          isPrime[i] = true;
      }

      for (int i = 2;i <= toNum;i++) {
          if (isPrime[i]) {
              primes.add(i);
          }
          for (int j = 0; j <= primes.size() && primes.get(j) * i <= toNum;j+=1) {
              isPrime[primes.get(j) * i] = false;
              if (i % primes.get(j) == 0) {
                  break;
              }
          }
      }
      return isPrime;

  由于每个合数仅被标记一遍,所以时间复杂度为O(n)。

posted @ 2023-01-02 19:47  iLoveFox  阅读(259)  评论(0)    收藏  举报