1. 质数计算问题概述
质数(素数)是指大于1的自然数中,除了1和它本身以外不再有其他因数的数。计算给定整数n以内的质数数量是计算机科学和数学中的经典问题,也是技术面试中的高频考点。力扣(LeetCode)第204题"Count Primes"要求实现一个高效算法,统计所有小于非负整数n的质数的数量。
这个问题看似简单,但存在几个关键挑战:
- 暴力解法的时间复杂度高达O(n√n),当n较大时(如n=5*10^6)会严重超时
- 需要理解并实现更高效的质数筛选算法
- 要考虑边界条件和特殊情况的处理
在实际工程中,质数计算广泛应用于密码学(如RSA算法)、哈希函数设计、随机数生成等领域。掌握高效质数判定和筛选算法是程序员必备的基础技能之一。
2. 常见解法与性能分析
2.1 暴力解法及其局限
最直观的解法是对每个小于n的数进行质数判断:
def countPrimes(n): count = 0 for num in range(2, n): if is_prime(num): count += 1 return count def is_prime(num): if num < 2: return False for i in range(2, int(num**0.5)+1): if num % i == 0: return False return True这种解法的时间复杂度为O(n√n),空间复杂度O(1)。当n=10^6时,需要进行约10^9次运算,这在力扣平台上会超时。
注意:判断质数时只需检查到√n即可,因为如果n能被大于√n的数整除,那么它必定已经被小于√n的数整除过。
2.2 埃拉托斯特尼筛法(Sieve of Eratosthenes)
埃氏筛法是一种更高效的质数筛选算法,基本思想是:
- 初始化一个长度为n的布尔数组is_prime,初始全为True
- 从2开始遍历,如果当前数是质数,则将其所有倍数标记为非质数
- 最后统计is_prime数组中为True的数量
实现代码:
def countPrimes(n): if n <= 2: return 0 is_prime = [True] * n is_prime[0] = is_prime[1] = False for i in range(2, int(n**0.5)+1): if is_prime[i]: for j in range(i*i, n, i): is_prime[j] = False return sum(is_prime)时间复杂度分析:
- 外层循环:O(√n)
- 内层循环:n/2 + n/3 + n/5 + ... ≈ O(n log log n)
- 总时间复杂度:O(n log log n)
空间复杂度:O(n),需要额外的布尔数组存储状态
2.3 欧拉筛法(线性筛法)
欧拉筛法是对埃氏筛法的优化,确保每个合数只被标记一次:
def countPrimes(n): if n <= 2: return 0 is_prime = [True] * n primes = [] for i in range(2, n): if is_prime[i]: primes.append(i) for p in primes: if i * p >= n: break is_prime[i * p] = False if i % p == 0: break return len(primes)时间复杂度:O(n),每个合数只被标记一次 空间复杂度:O(n),需要存储质数列表和布尔数组
3. 算法优化与细节处理
3.1 埃氏筛法的优化技巧
外层循环上限优化:只需遍历到√n,因为任何大于√n的数的倍数都已经被更小的质数标记过
内层循环起始点优化:对于质数p,可以从p²开始标记,因为2p,3p,...,(p-1)p已经被更小的质数标记过
步长优化:标记非质数时,可以以2p为步长(仅适用于奇数筛法)
位图压缩:使用位运算压缩存储空间,将内存使用减少到原来的1/8
优化后的实现:
def countPrimes(n): if n <= 2: return 0 sieve = [True] * n sieve[0] = sieve[1] = False for i in range(2, int(n**0.5)+1): if sieve[i]: sieve[i*i:n:i] = [False] * len(sieve[i*i:n:i]) return sum(sieve)3.2 边界条件处理
需要特别注意以下边界情况:
- n=0或1时,应返回0
- n=2时,小于2的质数数量为0
- 大数情况下的内存和时间限制
3.3 性能对比测试
对不同算法在n=10^6时的性能测试:
| 算法 | 时间复杂度 | 运行时间(ms) | 内存使用(MB) |
|---|---|---|---|
| 暴力法 | O(n√n) | >2000(超时) | <1 |
| 埃氏筛法 | O(n log log n) | 120 | ~8 |
| 欧拉筛法 | O(n) | 180 | ~10 |
| 优化埃氏筛 | O(n log log n) | 80 | ~8 |
4. 实际应用与扩展
4.1 质数计算在密码学中的应用
RSA加密算法依赖于大质数的难以分解性。典型的RSA密钥生成步骤:
- 随机选择两个大质数p和q
- 计算n=p×q
- 计算欧拉函数φ(n)=(p-1)(q-1)
- 选择公钥指数e,满足1<e<φ(n)且gcd(e,φ(n))=1
- 计算私钥指数d≡e^-1 mod φ(n)
4.2 质数分布规律
质数定理描述了质数在自然数中的分布情况:
- 小于n的质数数量π(n)≈n/ln(n)
- 第n个质数p_n≈n ln n
4.3 相关算法题扩展
- 质因数分解:将一个数分解为质数的乘积
- 最小公倍数/最大公约数:基于质因数分解或欧几里得算法
- 哥德巴赫猜想:任一大于2的偶数都可写成两个质数之和
5. 常见问题与调试技巧
5.1 算法实现中的常见错误
数组越界:在标记倍数时,确保不超过数组长度
- 错误示例:
for j in range(i*2, n, i)(当i很大时可能超出范围) - 正确做法:
for j in range(i*i, n, i)
- 错误示例:
初始条件错误:忘记将0和1标记为非质数
# 错误示例 is_prime = [True] * n # 忘记设置is_prime[0]和is_prime[1]类型错误:在Python中使用列表切片赋值时类型不匹配
# 错误示例 sieve[i*i:n:i] = False # 应该使用[False] * len(...)
5.2 性能优化技巧
使用更快的语言实现:对于极大n(>10^7),可考虑使用C++等编译型语言
分段筛法:处理极大数时,将区间分成小块处理以减少内存使用
并行计算:埃氏筛法的标记过程可以并行化
5.3 测试用例设计
全面的测试用例应包含:
- 边界情况:n=0,1,2
- 小质数:n=10(应返回4)
- 中等规模:n=100(应返回25)
- 大规模:n=10^6(应返回78498)
- 极大数:n=5*10^6(性能测试)
test_cases = [ (0, 0), (1, 0), (2, 0), (10, 4), (100, 25), (1000, 168), (10000, 1229), (5000000, 348513) ]6. 不同语言实现对比
6.1 C++实现(最高效)
int countPrimes(int n) { if (n <= 2) return 0; vector<bool> is_prime(n, true); is_prime[0] = is_prime[1] = false; for (int i = 2; i * i < n; ++i) { if (is_prime[i]) { for (int j = i * i; j < n; j += i) { is_prime[j] = false; } } } return count(is_prime.begin(), is_prime.end(), true); }6.2 Java实现
public int countPrimes(int n) { if (n <= 2) return 0; boolean[] isPrime = new boolean[n]; Arrays.fill(isPrime, true); isPrime[0] = isPrime[1] = false; for (int i = 2; i * i < n; i++) { if (isPrime[i]) { for (int j = i * i; j < n; j += i) { isPrime[j] = false; } } } int count = 0; for (boolean b : isPrime) { if (b) count++; } return count; }6.3 JavaScript实现
function countPrimes(n) { if (n <= 2) return 0; const isPrime = new Array(n).fill(true); isPrime[0] = isPrime[1] = false; for (let i = 2; i * i < n; i++) { if (isPrime[i]) { for (let j = i * i; j < n; j += i) { isPrime[j] = false; } } } return isPrime.filter(val => val).length; }7. 进阶话题与扩展阅读
7.1 概率性质数测试
对于极大数(>10^100),确定性算法效率太低,可以使用:
- Miller-Rabin测试
- Solovay-Strassen测试
- AKS测试(确定性多项式时间算法)
7.2 质数生成算法
随机质数生成:
- 随机生成一个大奇数
- 使用概率性测试检查是否为质数
- 重复直到找到质数
安全质数:形如p=2q+1的质数,其中q也是质数,用于密码学
7.3 数学理论深入
- 黎曼猜想:与质数分布密切相关的重要数学猜想
- 等差数列中的质数:狄利克雷定理
- 孪生质数猜想:存在无限多对相差2的质数
在实际编程竞赛和面试中,埃拉托斯特尼筛法是最常考察的质数计算算法。我建议熟练掌握其标准实现和优化版本,理解时间复杂度的计算方式,并能够处理各种边界条件。对于特别大的n(如超过10^8),可以考虑分段筛法或切换到更底层的语言实现。