如何在 Python 中检查一个数字是否为素数

Python 素数检测程序详解

本指南将深入探讨如何利用 Python 编程语言来判断一个数字是否为素数。

在编程面试或测试中,素数检测问题是很常见的。 通过本文,你将掌握解决这类问题的最佳方法。

本教程的主要内容包括:

  • 回顾素数的定义和基本概念。
  • 使用 Python 编写代码,有效地判断给定数字是否为素数。
  • 优化代码,使之达到 O(√n) 的运行时间复杂度。

下面,我们开始详细讲解。

素数的概念

让我们先复习一下素数的基础知识。

在数论中,自然数 n 被定义为素数,如果它仅有两个因数:1 和它本身。 回顾数学知识,如果 i 可以整除 n,那么 i 就是 n 的因数。✅

以下表格列出了一些数字、它们的全部因数以及它们是否为素数。

数字 (n) 因数 是否为素数?
1 1
2 1, 2
3 1, 3
4 1, 2, 4
7 1, 7
15 1, 3, 5, 15

从上表可以得出:

  • 2 是最小的素数。
  • 1 是任何数字的因数。
  • 任何数字 n 都是它自身的因数。

因此,1 和 n 是任何数字 n 的必然因数。素数不应有除了这两个之外的任何其他因数。

这意味着在 2 到 n-1 的范围内,应该找不到任何可以整除 n 而没有余数的非平凡因数。

O(n) 时间复杂度的素数检测算法

现在,我们将上述概念转化为 Python 函数。

可以使用 Python 的 range() 函数来遍历 2 到 n-1 之间的所有数字。

在 Python 中,`range(start, stop, step)` 会返回一个范围对象。你可以遍历这个范围对象,逐步得到从 start 到 stop-1 的序列。

因此,要得到 2 到 n-1 的整数集合,可以使用 `range(2, n)` 并将其结合 `for` 循环。

以下是算法的思路:

  • 如果找到任何能整除 n 的数字(余数为 0),即可判断该数字不是素数。
  • 如果遍历了 2 到 n-1 的所有数字,而没有找到任何可以整除 n 的数字,则该数字为素数。

Python 素数检测函数

根据上述思路,我们可以定义 `is_prime()` 函数如下:


def is_prime(n):
  for i in range(2,n):
    if (n%i) == 0:
      return False
  return True

现在来分析一下这个函数定义:

  • 函数 `is_prime()` 接收一个正整数 n 作为参数。
  • 如果在 (2, n-1) 的范围内找到一个因数,函数返回 `False`,表示该数字不是素数。
  • 如果在整个循环中没有找到因数,函数返回 `True`,表示该数字是素数。

接下来,用一些参数调用此函数,验证输出是否正确。


is_prime(2)
# True

is_prime(8)
# False

is_prime(9)
# False

is_prime(11)
# True

输出结果显示,2 和 11 是素数(函数返回 `True`),而 8 和 9 不是素数(函数返回 `False`)。

优化 `is_prime()` 函数

我们可以对函数进行一些优化。观察下表中的因数列表。

数字 因数
6 1, 2, 3, 6
10 1, 2, 5, 10
18 1, 2, 3, 6, 9, 18

我们发现,n 中唯一大于 n/2 的因数是 n 本身。

因此,不必循环到 n-1,只需循环到 n/2 即可。

如果在 n/2 之前没有找到因数,那么在 n/2 之后也不会找到因数。

现在,修改 `is_prime()` 函数以检测 (2, n/2) 范围内的因数:


def is_prime(n):
  for i in range(2,int(n/2)):
    if (n%i) == 0:
      return False
  return True

让我们通过函数调用验证输出。


is_prime(9)
# False

is_prime(11)
# True

虽然看起来进行了优化,但从运行时间复杂度来看,此算法与之前的算法效率相同,都是 O(n)。这意味着算法的运行时间与 n 的值成线性关系。

我们还能做得更好吗?答案是肯定的!

▶️ 请继续阅读,了解如何进一步优化素数检测的时间复杂度。

O(√n) 时间复杂度的素数检测算法

一个数的因数总是成对出现。

如果 a 是 n 的因数,那么一定存在另一个因数 b,使得 a × b = n,简而言之,ab = n。

通过示例验证一下:

以下表格展示了数字 18 的成对因数。你可以使用其他数字进行验证。

同时注意,√18 大约为 4.24。

在成对的因数 (a, b) 中,如果 a 小于 4.24,那么 b 一定大于 4.24。例如 (2, 9) 和 (3, 6)。

18 的成对因数:(1,18), (2,9), (3,6)

下图显示了数轴上 18 的因数:

如果数字 n 恰好是一个完全平方数,那么 a = b = √n 就是其中一个因数。

▶️ 观察表格中数字 36 的因数。由于 36 是一个完全平方数,所以 a = b = 6 是其中一个因数。对于其他因数对 (a, b),若 a < 6,则 b > 6。

36 的成对因数:(1,36), (2,18), (3,12), (4,9), (6,6)

总结如下:

  • 每个数字 n 都可以写成 n = a × b 的形式。
  • 如果 n 是完全平方数,则 a = b = √n。
  • 如果 a < b,则 a < √n 且 b > √n。
  • 如果 a > b,则 a > √n 且 b < √n。

因此,不必循环到 n/2,只需循环到 √n 即可。这比之前的方法效率高得多。

如何将 `is_prime()` 修改为 O(√n) 算法

现在,优化函数,检测 Python 中的素数。


import math
def is_prime(n):
  for i in range(2,int(math.sqrt(n))+1):
    if (n%i) == 0:
      return False
  return True

分析上面的函数定义:

  • 导入 Python 的内置 `math` 模块,使用 `math.sqrt()` 函数计算平方根。
  • 由于 n 可能不是完全平方数,所以必须将其转换为整数,用 `int(var)` 将 `var` 转换为整数。
  • 为了确保我们实际上检查到 √n,需要加一,因为 `range()` 函数默认不包含范围的终点。

以下代码单元验证 `is_prime()` 函数是否正常工作。


is_prime(8)
# False

is_prime(15)
# False

is_prime(23)
# True

在下一节中,创建简单的图表,直观理解 O(n) 和 O(√n)。

直观比较 O(n) 和 O(√n)

▶️ 在 Jupyter Notebook 环境中运行以下代码片段:


import numpy as np
import seaborn as sns
import pandas as pd

n = 20

x = np.arange(n)
y1 = np.sqrt(x)
y2 = x
df = pd.DataFrame({"O(√n)":y1,"O(n)":y2})
sns.set_theme()
sns.lineplot(data = df)

上述代码展示了如何绘制一系列值的 n 和 √n 曲线图。

  • 使用 NumPy 的 `arange()` 函数创建一个数字数组。
  • 将 20 以内(不包含 20)的 n 和 √n 值收集到 pandas DataFrame 中。
  • 使用 seaborn 的 lineplot() 功能绘制图表。

下图显示,√n 明显小于 n。

现在将范围扩大到 2000,重复上述步骤:


import numpy as np
import seaborn as sns
import pandas as pd


n = 2000

x = np.arange(n)
y1 = np.sqrt(x)
y2 = x
df = pd.DataFrame({"O(√n)":y1,"O(n)":y2})
sns.set_theme()
sns.lineplot(data = df)

从上图可以看出,在检测较大的数字是否为素数时,O(√n) 算法明显更快。

举一个简单的例子:2377 是一个素数,验证一下!😀

使用 O(n) 方法,需要大约 2000 步,而 O(√n) 算法只需 49 步即可得到答案。✅

结论

⏳ 现在来快速总结一下:

  • 检测一个数字是否为素数,最直接的方法是遍历 (2, n-1) 范围内的所有数字。如果没有找到能整除 n 的因数,则 n 为素数。
  • 由于 n 大于 n/2 的唯一因数是 n 本身,可以选择只遍历到 n/2。
  • 以上两种方法的时间复杂度均为 O(n)。
  • 由于一个数的因数总是成对出现,所以最多只需遍历到 √n 即可。此算法比 O(n) 快得多。检测大数是否为素数时,优势非常明显。

希望你掌握了如何在 Python 中检测素数。下一步,你可以查看我们关于 Python 字符串操作的教程,你将学习如何检测字符串是否为回文、字谜等。

下次教程再见。祝你编程愉快!👩🏽‍💻