你可能不知道两千多年前的算法有多厉害!

公众号关注 “GitHubDaily”

设为 “星标”,每天带你学编程!

我们都知道在数学领域,素数非常重要,有海量的公式和研究关于素数,比如那个非常著名至今没有人解出来的哥德巴赫猜想。

和数学领域一样,素数在信息领域也非常重要,有着大量的应用。

举个简单的例子,很多安全 加密算法 也是利用的质数。

我们想要利用素数去进行各种计算之前,总是要先找到素数。

所以这就有了一个最简单也最不简单的问题,我们怎么样来寻找素数呢?

一、判断素数

寻找素数最朴素的方法当然是一个一个遍历,我们依次遍历每一个数,然后分别判断是否是素数。

所以问题的核心又回到了判断素数上,那么怎么判断一个数是不是素数呢?

素数的性质只有一个,就是只有 1 和它本身这两个因数,我们要判断素数也只能利用这个性质。

所以可以想到,假如我们要判断 n 是否是素数,可以从 2 开始遍历到 n - 1 ,如果这 n - 1 个数都不能整除 n ,那么说明 n 就是素数。这个我没记错在 C 语言的练习题当中出现过,总之非常简单,可以说是最简单的算法了。

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

显然,这个算法是可以优化的。

比如当 n 是偶数的时候,我们根本不需要循环,除了 2 以外的偶数一定是合数。

再比如,我们循环的上界其实也没有必要到 n - 1 ,到就可以了。因为因数如果存在一定是成对出现的,如果存在小于根号 n 的因数,那么 n 除以它一定大于根号 n 。

这个改进也很简单,稍作改动即可:

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

这样我们把 O (n) 的算法优化到了 O (sqrt (n)) 也算是有了很大的改进了,但是还没有结束,我们还可以继续优化。

数学上有一个定理,只有形如 6n - 1 和 6n + 1 的自然数可能是素数,这里的 n 是大于等于 1 的整数。

这个定理乍一看好像很高级,但其实很简单,因为所有自然数都可以写成 6n,6n + 1,6n + 2 ,6n + 3 ,6n + 4 ,6n + 5 这 6 种,其中 6n ,6n + 2,6n + 4 是偶数,一定不是素数。6n + 3 可以写成 3 (2n + 1) ,显然也不是素数,所以只有可能 6n + 1 和 6n + 5 可能是素数。6n + 5 等价于 6n-1,所以我们一般写成 6n - 1 和 6n + 1 。

利用这个定理,我们的代码可以进一步优化:

def is_prime(n):
    if n % 6 not in (1, 5) and n not in (2, 3):
        return False
    for i in range(3, int(math.sqrt(n) + 1)):
        if n % i == 0:
            return False
    return n != 1

虽然这样已经很快了,但仍然不是最优的,尤其是当我们需要寻找大量素数的时候,仍会消耗大量的时间。那么有没有什么办法可以批量查找素数呢?

有,这个方法叫做埃拉托斯特尼算法

这个名字念起来非常拗口,这是一个古希腊的名字。此人是个古希腊的大牛,是大名鼎鼎的 阿基米德 的好友。

他虽然没有阿基米德那么出名,但是也非常非常厉害,在数学、天文学、地理学、文学、历史学等多个领域都有建树,并且还自创方法测量了地球直径、地月距离、地日距离以及黄赤交角等诸多数值。

要知道他生活的年代是两千五百多年前,那时候中国还是春秋战国时期,可以想见此人有多厉害。

二、埃式筛法

我们今天要介绍的埃拉托斯特尼算法就是他发明的用来筛选素数的方法,为了方便我们一般简称为埃式筛法或者筛法

埃式筛法的思路非常简单,就是用已经筛选出来的素数去过滤所有能够被它整除的数。这些素数就像是筛子一样去过滤自然数,最后被筛剩下的数自然就是不能被前面素数整除的数,根据素数的定义,这些剩下的数也是素数。

举个例子,比如我们要筛选出 100 以内的所有素数,我们知道 2 是最小的素数,我们先用 2 可以筛掉所有的偶数。然后往后遍历到 3 ,3 是被 2 筛剩下的第一个数,也是素数,我们再用 3 去筛除所有能被 3 整除的数。

筛完之后我们继续往后遍历,第一个遇到的数是 7 ,所以 7 也是素数,我们再重复以上的过程,直到遍历结束为止。结束的时候,我们就获得了 100 以内的所有素数。

如果还不太明白,可以看下面这张动图,非常清楚地还原了这整个过程。

  

这个思想非常简单,理解了之后写出代码来真的很容易:

def eratosthenes(n):
    primes = []
    is_prime = [True] * (n + 1)
    for i in range(2, n+1):
        if is_prime[i]:
            primes.append(i)
            # 用当前素数i去筛掉所有能被它整除的数
            for j in range(i * 2, n+1, i):
                is_prime[j] = False
    return primes

我们来分析一下筛法的复杂度,从代码当中我们可以看到,我们一共有了两层循环,最外面一层循环固定是遍历 n 次。

而里面的这一层循环遍历的次数一直在变化,并且它的运算次数和素数的大小相关,看起来似乎不太方便计算。

实际上是可以的,根据素数分布定理以及一系列复杂的运算(相信我,你们不会感兴趣的),我们是可以得出筛法的复杂度是 O (N ln ln N)。

三、极致优化

筛法的复杂度已经非常近似 O (n) 了,因为即使在 n 很大的时候,经过两次 ln 的计算,也非常近似常数了,实际上在绝大多数使用场景当中,上面的算法已经足够应用了。

但是仍然有大牛不知满足,继续对算法做出了优化,将其优化到了的复杂度  O (n)

虽然从效率上来看并没有数量级的提升,但是应用到的思想非常巧妙,值得我们学习。

在我们理解这个优化之前,先来看看之前的筛法还有什么可以优化的地方。

比较明显地可以看出来,对于一个合数而言,它可能会被多个素数筛去。比如 38 ,它有 2 和 19 这两个素因数,那么它就会被置为两次 False ,这就带来了额外的开销,如果对于每一个合数我们只更新一次,那么是不是就能优化到了呢?

怎么样保证每个合数只被更新一次呢?这里要用到一个定理,就是每个合数分解质因数只有的结果是唯一的

既然是唯一的,那么一定可以找到最小的质因数,如果我们能够保证一个合数只会被它最小的质因数更新为 False ,那么整个优化就完成了。

那我们具体怎么做呢?

其实也不难,我们假设整数 n 的最小质因数是 m ,那么我们用小于 m 的素数 i 乘上 n 可以得到一个合数。

我们将这个合数消除,对于这个合数而言,i 一定是它最小的质因数。因为它等于 i * n ,n 最小的质因数是 m , i  又小于 m ,所以 i 是它最小的质因数,我们用这样的方法来生成消除的合数,这样来保证每个合数只会被它最小的质因数消除。

根据这一点,我们可以写出新的代码:

def ertosthenes(n):
    primes = []
    is_prime = [True] * (n+1)
    for i in range(2, n+1):
        if is_prime[i]:
            primes.append(i)
        for j, p in enumerate(primes):
            # 防止越界
            if p > n // i:
                break
            # 过滤
   is_prime[i * p] = False
            # 当i % p等于0的时候说明p就是i最小的质因数
            if i % p == 0:
                break
                
    return primes

四、总结

到这里,我们关于埃式筛法的介绍就告一段落了。

埃式筛法的优化版本相对来说要难以记忆一些,如果记不住的话,可以就只使用优化之前的版本,两者的效率相差并不大,完全在可以接受的范围之内。

筛法看着代码非常简单,但是非常重要,有了它,我们就可以在短时间内获得大量的素数,快速地获得一个素数表。

有了素数表之后,很多问题就简单许多了,比如因数分解的问题,比如信息加密的问题等等。我每次回顾筛法算法的时候都会忍不住感慨,这个两千多年前被发明出来的算法至今看来非但不过时,仍然还是那么巧妙。

希望大家都能怀着崇敬的心情,理解算法当中的精髓

---

由 GitHubDaily 原班人马打造的公众号:GitCube,现已正式上线!
接下来我们将会在该公众号上,为大家分享优质的计算机学习资源与开发者工具,坚持每天一篇原创文章的输出,感兴趣的小伙伴可以关注一下哈!

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值