
1. 题目背景与核心需求这道来自厦门大学的机试题非素数个数看似简单却暗藏玄机。作为计算机专业学生必须掌握的经典题型它考察的是对素数判断算法的理解与优化能力。题目要求给定一个整数n统计小于n的所有非素数即合数和1的数量。在实际编程竞赛和面试中这类题目经常作为考察基础算法能力的试金石。我曾在某次校招笔试中遇到过几乎相同的变种题当时由于没有掌握筛法优化导致大规模数据时超时。这道题的价值在于基础层面训练循环结构和条件判断的编码能力进阶层面理解不同素数判断算法的时间复杂度差异工程层面掌握空间换时间的优化思想2. 素数判断算法对比分析2.1 暴力判断法试除法最直观的方法是逐个判断每个数是否为素数def is_prime(num): if num 2: return False for i in range(2, num): if num % i 0: return False return True时间复杂度O(n²)当n10⁶时现代计算机也需要数分钟才能完成计算。注意循环终止条件可以优化为range(2, int(math.sqrt(num)) 1)但时间复杂度仍为O(n√n)2.2 埃拉托斯特尼筛法埃氏筛更高效的解决方案是使用筛法def count_non_primes(n): if n 2: return 0 is_prime [True] * n is_prime[0] is_prime[1] False for i in range(2, int(math.sqrt(n)) 1): if is_prime[i]: for j in range(i*i, n, i): is_prime[j] False return n - sum(is_prime)时间复杂度O(n log log n)空间复杂度O(n)。对于n10⁶执行时间在毫秒级。3. 算法优化实战3.1 埃氏筛的位运算优化当n很大时如10⁸内存可能成为瓶颈。可以使用位图压缩存储def count_non_primes_bit(n): if n 2: return 0 size (n 7) // 8 sieve bytearray([0xFF] * size) def set_bit(num): sieve[num 3] ~(1 (num 7)) set_bit(0) set_bit(1) for i in range(2, int(math.sqrt(n)) 1): if sieve[i 3] (1 (i 7)): for j in range(i*i, n, i): set_bit(j) return n - sum(1 for i in range(n) if sieve[i 3] (1 (i 7)))内存占用减少为原来的1/8可以处理更大的n值。3.2 分段筛法处理超大范围当n达到10¹²级别时需要分段处理先用普通筛法预处理√n以内的素数将[0,n)区间分为多个块每块大小约√n对每个块用预处理的素数进行筛除4. 边界条件与特殊处理4.1 输入范围验证实际编码时需要考虑n为负数时的处理通常返回0n0或1时的特殊情况大整数支持Python无此问题但C/Java需注意4.2 性能测试对比在我的笔记本上测试i7-11800HPython 3.9方法n10⁴n10⁵n10⁶n10⁷暴力法0.12s12.3s5min-埃氏筛0.001s0.008s0.12s1.4s位运算优化0.001s0.006s0.09s1.1s5. 实际应用场景延伸素数筛法不仅是算法题宠儿在密码学、哈希算法等领域有重要应用RSA加密算法需要大素数生成布隆过滤器使用类似筛法的位操作哈希表大小常取素数减少冲突我在开发一个分布式ID生成器时就借鉴了筛法思想预生成素数池相比实时判断性能提升显著。6. 常见错误与调试技巧6.1 典型错误案例漏判1和0的非素数属性筛法未处理i*i可能溢出在C/Java中循环边界错误如range终点是否包含6.2 调试建议对小范围n如20打印中间结果使用assert验证特殊值assert count_non_primes(10) 5 # 1,4,6,8,9用timeit模块进行性能测试7. 不同语言实现要点7.1 C实现关键点vectorbool sieve(n, true); // 专用bool优化 for(int i2; i*in; i){ if(sieve[i]){ for(int ji*i; jn; ji){ sieve[j] false; } } }注意vector 是特化版本每个元素占1bit7.2 Java注意事项BitSet sieve new BitSet(n); sieve.set(0, n); // 全部初始化为true for(int i2; i*in; i){ if(sieve.get(i)){ for(int ji*i; jn; ji){ sieve.clear(j); } } }Java的BitSet比boolean[]更节省内存8. 算法竞赛进阶技巧8.1 欧拉线性筛当需要同时获取素数列表时线性筛更优def linear_sieve(n): primes [] is_prime [True] * n 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 primes时间复杂度O(n)每个合数只被标记一次8.2 多线程并行筛法对于超大规模n如n10⁹可以将区间分为多个段每个线程处理一个段共享预计算的√n以内素数表9. 数学优化思路利用数论知识可以进一步优化只处理奇数除2外偶数都不是素数使用轮式筛法跳过更多已知非素数概率性测试如Miller-Rabin用于极大数10. 实际工程经验在真实项目中我通常会预计算常用范围内的素数表并持久化使用LRU缓存最近查询结果对超范围请求降级为概率性测试曾经在金融系统开发中缓存素数表使交易签名性能提升40倍。关键是要理解算法选择永远需要权衡时间、空间和精度三大要素。