(三)LeetCode系列题型 | 素数


1. 简介

素数(质数)定义为:在大于1的自然数中,除了1和它本身外不再有其他因数的自然数,即在区间[2, n-1]范围内没有因数。本文将介绍几种常见的判断某个数是否为素数(质数)的方法以及寻找在某个范围内的素数个数。


2. 判断素数-枚举

枚举的思路是定义变量i从2开始遍历,如果该数能被i整除,则该数不是素数。程序如下:

bool isPrime(int x) {
	for (int i = 2; i < x; ++i) {
		if (x % i == 0) return false;
	}
	return true;
}

上述循环的遍历区间是[2, n-1],我们可以尝试对上述循环优化。考虑到如果y是x的因数,那么x/y也是x的因数,因此我们只需要判断y或者x/y即可。而如果我们每次仅判断二者中较小的那个,则较小数一定落在区间[2, sqrt(x)]内。因此,我们可以将遍历区间从[2, n-1]优化到[2, sqrt(x)]。程序如下:

bool isPrime(int x) {
    if (x <= 1) return false;
	for (int i = 2; i * i <= x; ++i) {
		if (x % i == 0) return false;
	}
	return true;
}

我们可以尝试继续优化:实际上,素数有一个特点即它总是等于6x-1或6x+1,其中x是大于等于1的自然数。证明如下:6x不为素数,6x+2不为素数,6x+3不为素数,6x+4不为素数。那么素数只可能出现在6x+1和6x-1位置处。所以我们可以将循环步长设定为6,然后每次仅判断6x+1和6x-1位置处的数即可。程序如下:

bool isPrime(int x) {
	// 判断小于5的数字
    if (x <= 3) return x > 1;
    // 如果不是6x-1或6x+1,则肯定不是素数
    if (x % 6 != 1 && x % 6 != 5) return false;
	for (int i = 5; i * i <= x; i += 6) {
		// i表示6x-1的位置,i+2表示6x+1的位置
		if (x % i == 0 || x % (i + 2) == 0) return false;
	}
	return true;
}

3. 计数素数-埃式筛

计数素数的要求找到在给定区间内的素数个数。如输入为n = 5,则返回结果为3(2和3两个素数、小于n)。

直观上,我们可以直接从2到n-1遍历,再使用上一节中判断素数的方法来判断每一个元素。但其实存在更为简便的方法,利用数与数之间的相关性,Eratosthenes提出埃式筛方法。埃式筛的思路是:如果x是素数,那么大于x的x的倍数2x,3x,…一定不是素数,因此我们可以将这些数做上标记以避免重复判断。具体做法是:首先我们定义一个数组isPrime,大小为n。在遍历的过程中通过对该数组赋值以标识当前元素是否为素数。结合上述思路,程序如下:

int countPrimes(int n) {
	vector<int> isPrime(n, 1);
	int cnt = 0;
	for (int i = 2; i < n; ++i) {
		if (isPrime[i]) {
			++cnt;
			// 合数标记
			for (int j = 2 * i; j < n; j += i)
		    	isPrime[j] = 0;
		}
	}
	return cnt;
}

在上述程序中,其实存在大量的重复标记。考虑输入为10,在i为2时4和8被标记;在i为4时8又被标记。当输入数字很大时,重复标记的操作会明显增多。实际上,在我们遍历到i时,i * i前面的数据肯定已经被标记过。考虑输入为20,在i为3时即i * i等于9时,前面的数据已经被全部标记(4被2标记,6被2标记、8被2标记)、在i为5时即i * i等于25,此时已经完成全部标记。优化后的程序如下:

int countPrimes(int n) {
	vector<int> isPrime(n, 1);
	int cnt = 0;
	for (int i = 2; i < n; ++i) {
		if (isPrime[i]) {
			++cnt;
			// 合数标记
			if ((long long)i * i < n)
				for (int j = i * i; j < n; j += i)
		    		isPrime[j] = 0;
		}
	}
	return cnt;
}

4. 计数素数-线性筛

上述优化后的埃式筛其实还是存在重复标记合数的操作,而线性筛的优化目标是仅标记一次合数。其具体做法是多维护一个数组primes用于存放当前得到的素数集合。与埃式筛中仅当前数为素数时才进行倍数标记不同的是,线性筛将素数集合中的数与当前数相乘(不管其是否为素数),且在出现整除时结束标记。

直观上,如果当前数x可以被primes[i]整数,那么对于合数y = x·primes[i + 1]而言,它一定在后面遍历到x / primes[i]·primes[i + 1]这个数时会被标记。这就保证了每个合数只会被其最小的质因数标记,即每个合数仅会被标记一次。程序如下:

int countPrimes(int n) {
	vector<int> primes;
	vector<int> isPrime(n, 1);
	for (int i = 2; i < n; ++i) {
		// 将素数加入素数集合中
		if (isPrime[i]) primes.push_back(i);
		for (int j = 0; j < primes.size() && i * primes[j] < n; ++j ){
			// 基于素数集合开始标记
			isPrime[i * primes[j]] = 0;
			// 终止标记
			if (i % primes[j] == 0) break;
		}
	}
	return primes.size();
}

5. 素数总结

本文总结了判断素数以及计数素数的几种经典解法,其他方法请参考参考部分


参考

  1. https://leetcode-cn.com/problems/count-primes/.


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值