(四)质数(素数)

专栏:算法

上一篇:(三)位运算

下一篇:

一、质数(素数)

1. 质数的定义

  质数:指在大于1的自然数中,除了1和它本身以外不再有其他因数自然数(因数一般是指正因数,后文也是如此)

  如 7 = 1 × 7 7 = 1 \times 7 7=1×7,没有其它正整数乘积的组合,所以7的因数只有 1 和 7,7是质数。在大于1的自然数中,如果一个数除了1和它本身外,还有其它的因数,那么这个数是合数。大于1的自然数不是质数就是合数。
   又如 6 = 1 × 6 = 2 × 3 6 = 1 \times 6 = 2 \times 3 6=1×6=2×3,可以得知6的因数有 1, 2, 3, 6共四个,所以 6 是合数。
   我们讨论的质数和合数是在大于1的自然数范围内,所以 0 和 1既不是质数也不是合数,负数也不在质数和合数的范围内。

  最小的质数是2,最小的合数是4。

  下图为100以内的质数,质数用蓝色标识,合数用白色标识。

在这里插入图片描述

2. 质数性质

性质
1质数的因数只有1和它本身。
2任一大于1的自然数,要么本身是质数,要么可以分解为几个质数之积,且这种分解是唯一的。
3质数的个数是无限的。
4 n n n 为正整数,在 n 2 n^{2} n2 ( n + 1 ) 2 (n+1)^{2} (n+1)2 之间至少有一个质数。
5 n n n 为大于或等于2的正整数,在 n n n n ! n! n! 之间至少有一个质数。
6若质数 p p p 为不超过 n ( n ≥ 4 ) n ( n \geq 4) n(n4) 的最大质数,则 p > n 2 p > \dfrac{n}{2} p>2n.
7对于一个足够大的整数N,不超过N的质数大约有 N ln ⁡ N \dfrac{N}{\ln N} lnNN个。

3. 质数的分布

3.1 除了2以外的质数都是奇数

  偶数是能被2整除的整数,所以除了2以外,其它偶数都不符合质数的定义。因此除了2以外的质数都是奇数,不是偶数。

3.2 除了2和3以外的质数都是6的倍数的相邻数

  在上面我们已经排除了偶数,下面我们再排除能被3整除的数。
  如下图所示,我们将数每连续6个排成一排,得到6列数(这里不考虑1,所以没有列出)

在这里插入图片描述

  每一列都用一个通项公式表示,得到下面的结果

列的通项公式 ( n ⩾ 1 ) (n \geqslant 1) (n1) 6 n − 4 6n-4 6n4 6 n − 3 6n-3 6n3 6 n − 2 6n-2 6n2 6 n − 1 6n-1 6n1 6 n 6n 6n 6 n + 1 6n+1 6n+1
因数 2 2 2 3 3 3 2 2 2 2 2 2 3 3 3

  可以发现,只有 6 n − 1 6n - 1 6n1 6 n + 1 6n + 1 6n+1 这两列不能被2或3整除,其余4列都是2或3的倍数,因此除了2和3以外,其它质数只能位于这两列,否则就能被2或3整除。得到结论:除了2和3以外的质数都是6的倍数的相邻数。

  当然,这只是说质数都是6的倍数的相邻数,并不是说只要满足这个条件的数都是质数。

4. 质因数

  质因数即一个正自然数的因数中是质数的因数。

  一个质数的因数只有1和它本身,如7的因数有1和7,而合数除了1和本身外,还有其它的因数。因数一般是成对的,数量为偶数,平方数则比较特殊,因数数量为奇数,不成对的一个因数值为 n \sqrt{n} n

4.1 质因数分解

在这里插入图片描述

  任何大于1的自然数,都成唯一表示成有限个质数乘积的形式。
  质数就等于它本身,不能再被分解,而合数则至少可以分解成两个以上的质数的乘积。如质数 7 = 7 7 = 7 7=7,合数 60 = 2 × 2 × 3 × 5 60=2 \times 2 \times 3 \times 5 60=2×2×3×5
  每个数的质因数分解形式是唯一的(只考虑质因数的组合),且一定存在的。

4.2 互质数

  公因数只有1的两个非0自然数,叫做 互质数,也称作这两个数互质。

在这里插入图片描述

  大于1的自然数都能分解成质因数相乘的形式,如果两个数互质,说明它们分解出的质因数中没有一个是相等的。如 70 和 33互质, 70 = 2 × 2 × 7 × 5 70= 2 \times 2 \times 7 \times 5 70=2×2×7×5, 33 = 3 × 11 33 = 3 \times 11 33=3×11

  最简分数:分子和分母是互质数的分数称为最简分数,分子和分母除了1外没有其它公因数,无法通过同除以公因数来简化,如 9 8 \dfrac{9}{8} 89。而 15 9 \dfrac{15}{9} 915 可以通过分子分母同除以3来化简成 5 3 \dfrac{5}{3} 35

二、质数的判定

  在判定一个自然数 n n n 是否是质数时,一般是采用质数的定义来进行判断:

  质数除了1和它本身以外不再有其他因数

  因此可以尝试将这个数 n n n 除以 [ 2 , n − 1 ] \left[2, n-1 \right] [2,n1] 范围内的自然数,来检查 n n n 是否有除1和它本身外的其它因数。

情况推断
n n n 能被 [ 2 , n − 1 ] \left[2, n-1 \right] [2,n1] 范围内的某一个自然数整除 n n n 不是质数
[ 2 , n − 1 ] \left[2, n-1 \right] [2,n1] 范围内的自然数都不能将 n n n 整除 n n n 是质数

  这叫做试除法。另外可以通过质数的一些性质减少不必要的试除,提高效率。
  如果是算出一个范围内所有质数,那么还可以用筛除法。筛除法的做法是将合数筛除掉,那么筛除后剩下的就是质数了。

1. 试除法对单个数进行判定

  试除法比较一般的做法:对一个待判定的大于1的自然数 n n n,在 [ 2 , n − 1 ] \left[2, n-1 \right] [2,n1] 范围内查找是否存在能将 n n n 整除的整数。
  ① 如果存在,那么 n n n 就有除了1和它本身以外的其他因数,也就是说 n n n 不是质数。
  ② 如果不存在,那么 n n n 就是质数。

1.1 试除法实现

在这里插入图片描述

  在 [ 2 , n − 1 ] \left[2, n-1 \right] [2,n1] 范围内查找是否存在能整除 n n n (即余数为0) 的整数。如果其中有一个数 a a a能整除 n n n,就是表示 a a a n n n 的因数,那么 n n n 就不是质数。如果试除完所有数后都没有整除的情况,那么 n n n 就是质数。

  但是有些需要注意的地方,质数是大于1的自然数。上面的处理并不能完全将小于等于1的数判定为不是质数,所以需要对小于等于1的整数作额外处理。

bool isPrime(int n)
{
	//小于等于1的数的非负数判定
	if (n <= 1)
		return false;
	
	//对2 ~ (n-1)之间的数进行遍历,分别求余,如果存在一个余数不为0的情况则n不是质数
	for (int i = 2; i < n; i++) {
		if ( n % i == 0)
			return false;
	}
	//不能被2~(n-1)的任何数整除,n为质数
	return true;
}

1.2 试除法的进一步优化

  可以根据一些特征排除掉部分合数,或者根据一些关系减少试除范围,从而对试除法进行优化。

在这里插入图片描述

1.2.1 缩小试除范围

  由因数的定义,可知因数是成对出现的,若 n = a ⋅ b n = a \cdot b n=ab (其中 a , b , n a,b,n a,b,n 都是非0自然数),那么 a a a b b b n n n 的一组因数, a a a b b b 中必定一个数不大于 n \sqrt{n} n

  如果 a a a b b b 两个都大于 n \sqrt{n} n ,那么乘积不可能等于 n n n

  所以只需要试除到 n \sqrt{n} n 即可,这样试除范围就能缩小到 [ 2 , n ] [2, \sqrt{n}] [2,n ]

double sqrtOfN = sqrt(n);

for (int i = 0; i <= sqrtOfN; i++)
{ }

  如果不使用平方根运算,还可以用 i 2 ≤ n i^{2} \leq n i2n来判断是否符合范围。

for (int i = 0; i * i <= n; i++)
{ }
1.2.2 只查找质因数

  试除的目的是查找出 n n n 除了 1 和它本身外是否还有其它因数,只需要是知道有还是没有即可,并不需要查找出所有的因数。
  合数能被分解成多个质因数相乘,如果一个数能被合数整除,那么一定能被这个合数的质因数整除。所以我们只需要查找质数即可,合数不需要再重复判断。例如,一个数如果不能被 2 整除,那么它肯定不能被 2 的倍数整除。
  在前面我们已经知道,除了 2 和 3 外,其它质数都是与 6 的倍数相邻的。因此我们只需要试除 2、3还有与 6 的倍数的相邻数即可,这样就直接可以减少 2 3 \dfrac{2}{3} 32 的数。

   6 的倍数的相邻数通项公式是 6 n − 1 6n-1 6n1 6 n + 1 6n+1 6n+1,但是质数 2 和 3 不符合这个式子,所以这里额外对2和3进行试除。

在这里插入图片描述

  最终,试除范围减小至 [ 2 , n ] [2, \sqrt{n}] [2,n ] 中6的倍数的相邻数,数量为 n 3 \dfrac{\sqrt{n}}{3} 3n ,复杂度为 O ( n ) O(\sqrt{n}) O(n )

bool isPrime(int n)
{
	//小于等于1的数的不是质数
	
	if (n <= 1)
		return false;
	
	//2和3的倍数不是质数(不包括2和3)
	if ((n > 3) && ((n % 2 == 0) || (n % 3 == 0)))
	 	return false;
	 	
	//对2 ~ sqrt(n)之间与6相邻的数(可能的质数)进行遍历,如果出现能被整除的情况则不是质数
	for (int i = 6; (i-1) * (i-1) <= n;  i += 6) {
		if ( (n % (i-1) == 0) || (n % (i + 1) == 0))
			return false;
	}
	//不能被2~ sqrt(n)的任何数整除,则为质数
	return true;
}

2. N N N 以内的所有质数

2.1 穷举法

在这里插入图片描述

  穷举法即直接用试除法判断范围内的各个数是否是质数。这个方法是比较容易想到的,遍历的复杂度是 O ( n ) O(n) O(n),而试除法判断一个数是否是质数的复杂度是 O ( n ) O(\sqrt{n}) O(n ),所以复杂度是 O ( n n ) O(n \sqrt{n}) O(nn )

void findPrime(int n)
{
	// 遍历[2, n]范围内的所有数
	for (int i = 2; i < n; i++) {
		// 判断是否是质数,是则输出
		if (isPrime(i))
			printf("%d ", i);
	}
	printf("\n");
}
2.1.1 穷举法的优化思路

  我们的目的是要找出质数,如果已经知道了哪些不是质数,那就不用再去通过计算来判断了。前面已经提到,除2和3以外,所有质数都分布于 6 n 6n 6n 的两边,所以只需要检测 6 n − 1 6n-1 6n1 6 n + 1 6n+1 6n+1 即可 ( n ⩾ 1 ) (n \geqslant 1) (n1),这样就减少了多余的判断。

  当然, isPrime() 里面本身已经对2和3的倍数快速筛除了,这只能稍微减少耗时,并不能大幅度减少。

void findPrime(int n)
{
	if (n >= 2)
		printf("2 ");
	if (n >= 3)
		printf("3 ");

	for (int i = 6; i - 1 <= n; i += 6) {
		if (isPrime(i-1))
			printf("%d ", i-1);
		if (i+1 <= n && isPrime(i+1))
			printf("%d ", i + 1);
	}
}

2.2 素数筛法

  素数筛是通过将范围内的合数筛除的方法找出质数。素数筛需要 额外的空间去标记每一个数是否是质数,所以需要占用较大的空间,空间复杂度为 O ( n ) O(n) O(n)

素数筛复杂度
埃式素数筛 O ( n log ⁡ log ⁡ n ) O(n \log{\log n}) O(nloglogn)
欧式素数筛 O ( n ) O(n) O(n)
2.2.1 基本原理

  素数筛并不直接检测某个数是否是质数,那样的话就需要对每个数以 O ( n ) O(\sqrt{n}) O(n ) 的复杂度进行计算,复杂度较高。前面已经说过,合数可以分解成质因数乘积的形式,也就是说,合数一定是某个比它的质数的倍数。这样,在找到一个质数后,我们就能直接将是它倍数的合数筛去,而不用以 O ( n ) O(\sqrt{n}) O(n ) 的复杂度对其进行判定。
  随着合数不断地被筛除,最后剩下的就是范围内所有的质数了。

请添加图片描述

2.2.2 埃拉托斯特尼筛法 (Sieve of Eratosthenes)

  埃拉托斯特尼素数筛的做法是先根据要求解的范围大小创建一个用于记录每个数是否是质数的数组,然后从2开始遍历,每遍历到一个数,如果没有被标记为合数,那么它就是质数,然后将它的位于求解范围内的所有倍数 标记为合数

  即如果 p p p 是找到的质数,则把在 [ 0 , n ] [0, n] [0,n]范围内的 2 p 2p 2p 3 p 3p 3p 4 p 4p 4p,…, k p kp kp 都标记为合数,其中 ( k p ⩽ n ) kp \leqslant n) kpn)

  标记完后遍历下一个数,直至遍历到一个合适的值为止(确保能将范围内的所有合数筛去即可,总数的平方根)。遍历完成后,数组中没被标记为合数的就是质数。

  合数 n n n 的质因数其中总会有一个不大于 n \sqrt{n} n ,因此当我们将 [ 2 , n ] [2, \sqrt{n}] [2,n ] 范围中的质数的倍数筛除完后, 如果 [ 2 , n ] [2, n] [2,n]中有合数,那肯定已经被筛去。这样就确保当我们将 n n n 的倍数筛去后, n + 1 n+1 n+1 如果没被筛去那 n + 1 n+1 n+1 就肯定是质数。

在这里插入图片描述

2.2.2.1 实现

  下面直接将找到的质数输出。
  考虑到合数 n n n 的最小质因数不大于 n \sqrt{n} n ,遍历到 n \sqrt{n} n 就能找到 n n n的质因数,因此遍历到 n \sqrt{n} n 即可。
  要将一个质数 p p p 的倍数筛除时,从 p 2 p^2 p2 开始筛除。因为 2 p , 3 p , ⋯   , ( p − 1 ) p 2p, 3p, \cdots, (p-1)p 2p,3p,,(p1)p,已经被之前的 2 , 3 , ⋯   , ( p − 1 ) 2, 3, \cdots, (p-1) 2,3,,(p1) 筛除过了,不需要重复筛除,因此从 p 2 p^2 p2 开始即可 。

/**素数筛, 筛除 [0, n]范围内的合数
 * numbers:外部创建的一个长度为(n+1)的数组。true 表示质数。
 */
void primeSieve(bool numbers[], int n)
{
	//初始化,标记为质数
	memset(numbers, 1, sizeof(bool) * (n + 1));
	numbers[0] = numbers[1] = false;
	
	//从2开始遍历,直到sqrt(n)
	for (int i = 2; i * i <= n; i++) {
		//如果自然数i被标记为质数,输出i并将其倍数标记为合数
		if (numbers[i]) {
			//从 i * i 开始,将i的倍数标记为合数
			for (int j = i * i; j <= n; j += i)
				numbers[j] = false;
		}
	}
}
2.2.3 欧拉筛法(Sieve of Euler)

  埃拉托斯特尼素数筛会出现一个数被多个质数同时筛掉的情况,如果一个合数有多个质因数,就会被筛去多次。
  如下所示,质数2和3的倍数中都有6和12,所以6和12会被2和3重复筛去。而 8 是质数2的倍数,其它质数的倍数中没有8,所以只会被2筛去一次。可以发现,合数被筛去的次数等于其质因数的个数
在这里插入图片描述
  埃拉托斯特尼素数筛的非线性复杂度就是因为有多个质因数的合数会被重复筛去,欧拉素数筛就解决了这个问题,通过避免重复的筛除从而将复杂度降低至线性

2.2.3.1 只选取最小质因数来筛除

  埃拉托斯特尼素数筛会将每个质数的倍数都筛除掉,并不检测是否会重复。如果一个合数是多个质数的倍数,或者说有多个质因数的话,那么这个合数就会被它的质因数各筛除一次。

  欧拉素数筛选取一个合数的最小质因数来将其筛除。

  假设 n n n 是质数 a a a m m m 倍,有 n = a m n =am n=am。那么如何确定 a a a是否是 n n n的最小质因数呢?
  如果 m m m 的最小质因数 b b b a a a 小,那么 n n n 的最小质因数应该是 b b b 而不是 a a a n n n 应该由 b b b 来筛除而不是 a a a

在这里插入图片描述
  所以在欧拉素数筛中有一个关键的地方就是,会将检测出质数从小到大保存到数组中,每检测到一个数 n n n时,都会将这个数与之前保存的质数(如果 n n n 是质数, n n n也会被保存进去)的乘积筛除,同时检查是否是质因数,一旦找到它的最小质因数 p p p,那么后的质数就不用再考虑了,那些都不会是最小质因数。

2.2.3.2 实现
/**素数筛, 筛除 [0, n]范围内的合数
 * numbers:外部创建的一个长度为(n+1)的数组。true 表示质数。
 */
 
void primeSieve(bool numbers[], int n)
{
	//初始每一个数都被标记为质数
	memset(numbers, true, sizeof(bool) * (n + 1));
	numbers[0] = numbers[1] = false;

	int* primes = (int*)malloc(sizeof(int) * (n / 3 + 2));
	int length = 0;

	for (int i = 2; i <= n/2; i++) {
		//是质数就加到primes数组里
		if (numbers[i]) 
			primes[length++] = i;

		//筛掉值为i * primes[j]的合数
		for (int j = 0; j < length; j++) {
			// 大于n的值无需筛除
			if (i * primes[j] > n)
				break;

			//将质数和i的乘积被筛掉
			numbers[i * primes[j]] = false;

			//如果i能被当前质数整除,后面更大的质数就不用再继续了,因为不是合数的最小质因数
			if (i % primes[j] == 0)
				break;
		}
	}

	free(primes);
}

三、质因数分解

  任一大于1的自然数,要么本身是质数,要么可以分解为几个质数的乘积,且这种分解是唯一的。

1. 简单实现

在这里插入图片描述

  对于一个给定的自然数 n ( n ⩾ 1 ) n(n \geqslant 1) n(n1),如果要将其分解成质因数乘积的形式:
  ① 令 p = n p = n p=n, 将 i i i从最小的质数2开始。
  ② 用 i i i 去试除 p p p,如果 i i i 能被 p p p 整除,那么 i i i 将是 n n n 的其中一个质因数,记录下来,然后赋值 p = p / i p = p / i p=p/i
  ③ 因为质因数是会重复的,所以需要用 i i i 试除到不能再整除为止,将质因数中的 i i i 全部提取出来后,再将 i i i递增至下一个自然数。
  ④ 重复第②③步,直到 i i i 递增至 大于 p p p 为止。

  因为合数都是由比它小的质数相乘得来,如果是 i i i是合数且 最开始的 n n n 所以在 i i i从2开始逐个递增的过程中,只有当 i i i是质数的时才能整除 n n n

void primeFactor(int n)
{
	// n小于2时无法分解,直接输出
	if (n < 2) {
		printf("%d", n);
		return;
	}
	
	//从2开始不断试除,直至大于n 
	for (int i = 2; i <= n; i++) {
		while (n % i == 0) {
			printf("%d ", i);
			n /= i;		//注意这里需要将n除以i
		}
	}
		
	printf("\n");
}

2. 进一步优化

  由于自然数 n n n 的因数是成对的关系: n = a × b n=a \times b n=a×b,如果 a > n a > \sqrt{n} a>n ,那么 b = n a b = \dfrac{n}{a} b=an就必定小于 n \sqrt{n} n 。因此对于一个合数 n n n,它的质因数中必定有一个小于等于 n \sqrt{n} n ,因此只需试除到 n \sqrt{n} n 就能找到它的因数将其整除。
  每提取出 n n n 的一个质因数 a a a n n n 都缩小至 n a \dfrac{n}{a} an,所以 n n n 的值会不断变小,最终变为其最大的质因数。

在这里插入图片描述

  因为查找的是质因数,之前已经说过,除了 2 和 3 外,其它质数都与 6 的倍数相邻,所以只需要用 6 i − 1 6i-1 6i1 6 i + 1 6i+1 6i+1 ( i ⩾ 1 ) (i \geqslant 1) (i1)去试除即可(2 和 3 需要额外处理)。

#include <math.h>
#include <stdio.h>

// 将n试除以i,直到不能整除为止
void continueDivide(int* n, int i)
{
	while (*n % i == 0) {
		printf("%d ", i);
		*n  /= i;
	}
}

//质因数分解
void primeFactor(int n)
{
	if (n < 2) {
		printf("%d ", n);
		return;
	}

	//单独分解出2和3
	continueDivide(&n, 2);
	continueDivide(&n, 3);

	//开平方根,避免误差,向上取整,多试除一个对结果无影响
	int sqrtOfN = int(ceil(sqrt(n)));
	int oldN = n;
	
	for (int i = 6; (i - 1) <= sqrtOfN ; i += 6) {
		continueDivide(&n, i-1);
		continueDivide(&n, i+1);

		// 如果 n 出现变化,重新计算平方根
		if (oldN != n) {
			oldN = n;
			sqrtOfN = int(ceil(sqrt(n)));
		}
	}
	
	//如果最后n不等于1,则为最大的质因数
	if (n != 1)
		printf("%d", n);
	printf("\n");
}

  最后有一个需要注意的点,上面的代码使用了连除,连除时并不检查是否超出了 n \sqrt{n} n ,最后可能会出现连除使得 n n n 变为 1。因此最后需要判断一下 n n n 是否为1来确定结果是否是其中一个质因数。

至于试除范围,假设 n n n 的最大质因数为 p p p,那么:

  • 如果 p p p 出现重复(如 18 = 2 × 3 × 3 18=2 \times 3 \times 3 18=2×3×3, 最大质因数3重复),试除范围为 [ 2 , p ] \left[ 2, p \right] [2,p]
  • 如果 p p p 不重复(如 12 = 2 × 2 × 3 12=2 \times 2 \times 3 12=2×2×3, 最大质因数3不重复),试除的范围为 [ 2 , p ] \left[ 2, \sqrt{p} \right] [2p ],(如果 p p p 小于 4,那么只试除 2)

  在最坏情况下( n n n 本身是个很大的质数),试除范围为 [ 2 , n ] [2, \sqrt{n}] [2,n ]


专栏:算法

上一篇:(三)位运算

下一篇:

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依稀_yixy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值