估算一下你的背包能装下几瓶可乐,这不叫数学题,这叫生活场景。而质数判断,往往就是程序员面对一道没完没了的加法题时,突然丧失了耐心的时候。别急着去背一堆死记硬背的定理,咱们就聊聊如何把数字拆解开看,看看它们到底是不是“正经”的整数。 大量初学者一上来就嫌费事,恨不得直接写个函数跳那会儿。
实际上不然,质数这东西,特爱开玩笑。5 是质数,7 也是,那 11、13、17 接着排着队登场。
要是我们把 1 排除在外,剩下 2, 3, 5, 7, 11, 13……这种不能整除的数,如何定义才算质数呢?最直观的算法就是:只要一个数大于 2,只要它不能被从 2 到它根号以内的任何整数整除,那它就是个质数。
这个逻辑实际上挺好办的,就像筛子一样,把能筛出来的都筛掉,剩下的就是质数了。 不过在实际开发里,写每一层判断都忒耗时了。
要是你要处理一个亿级的数字数组,直接对每个数都试除法,那 CPU 估摸都要烧成灰。
这时候就需求更智慧的方式了。
比方说,偶数除了 2 以外的肯定不是质数,只要是 2 的倍数,直接跳过。
同样,3 只能被 1 和 3 整除,那 5 的倍数除了 5 和 5 的平方根,其他都不能是质数。
这种处理技巧,在 C++ 要么 Python 里都能写得挺顺滑,只要懂得利用“最小因子”来提前截断循环。 说到例子,就拿 1000000007 来说吧。
这个数在计算机里是个挺常见的数值。假设我们要判断它是不是质数,按照最小因子算法,我们只需求检查它是不是 2 的倍数(显然不是),是不是 3 的倍数(也不是,1+0+0+0+0+0+0+0+7=8,8 不能被 3 整除)。
接着检查 5,显然最终是 7,不能用 5 整除。
这时候我们只需检查到它的平方根,也就是大约 31623。在这个范围内,要是找不到任何能整除它的数,那它就是个质数。
这就好比玩猜拳,对方没动手,你就赢了。 还有一种情况,就是平方根。
比如 25,它的平方根是 5。检查 2 到 5 之间(不含 5),有没有能整除 25 的数?2 不能整除 25,3 也不能,4 也不中。
故此 25 是合数。
这里有个小细节,判断 1 的时候要小心,1 既不是质数也不是合数,它是个特殊数。
要是你写了个死循环,直接输出 1,那代码就会报错要么死机。
故此最好还是单独写个 if 语句处理,要么在循环判断前就把 1 给过滤掉。 再讲讲大一点的数,比如 97。它的平方根是大约 9.8,故此只需求检查到 9。2, 3, 4, 5, 6, 7, 8 都不能整除 97。
故此 97 是质数。
这时候你会发现,大质数的表空间特别大,要是一个数存起来,可能需求几兆就连几十兆的内存。
这涉及到浮点数精度难题,有时候直接转换字符串再转回数字再算平方根,精度会丢失,害得误判。
这时候得用整数开根号的方式,要么用欧拉判别式之类的数论知识来辅助判断。 自然,对于极端的大数,比如 2^64 这种无法直接存进整型变量里的数,那就务必得用 BigInt 类了。
这时候再试除法的复杂度就飙升了,可能需求用到 Miller-Rabin 素性测试。
这个算法的核心思想是随机抽样,通过特定的概率论模型,以极高的准率判断一个数是不是质数。别看听起来挺玄乎,但实际运行速度比试除法快多了,特别是在处理那些庞大的、看似随机分布的整数时,效率差异庞大。 实际上质数判断最有趣的地方在于“下一个质数”的难题。
比如我要找第 1000 个质数,不用一个一个找,能够利用之前的质数列表进行筛法。埃拉托斯特尼筛法,要么叫 Sieve of Eratosthenes,这个技术略微有点绕,但原理挺朴素:先把所有 2 的倍数涂成黑色,然后把所有 3 的倍数(除了已经被涂掉的局部)涂黑,接着处理 5,6……直到涂完所有的数。剩下的白色格子就是质数。
这个方式 Time O(n log log n),模板化程度挺高。在大数计算场景下,比如处理哈希表中的 key 验证,要么在算法竞赛中解决数论难题,筛法往往是一绝。 还有啊,最近十年数学界有个大新闻,那个 2^64 - 1 被认定是目前为止最大的已知质数。
这玩意儿忒大了,根本放不下,只能靠计算机那庞大的内存池来存。
要是你在一个程序里直接硬塞这个数,内存溢出是迟早的事。
这就是为啥程序员在写代码时,尽量把判断逻辑封装起来,避免硬编码大数。 最终总结一下,质数判断这事儿,核心就两点:一是小范围用试除法或筛法,二是大范围用Miller-Rabin要么斯特林筛。别总想着把公式记陈年,写代码最关键的是写出清楚的逻辑和高效的实现。当你面对一个庞大的整数数组时,别急着写 foreach 循环,想想能不能用位运算优化,要么直接用筛法预处理一遍,往往能解决 80% 的难题。生活里,质数无处不在,从分糖果的数量到彩票的中奖号码,从你敲键盘的活跃度到数据库里的记录条数,它们都在定义着数据的纯净与秩序。