深入C语言底层系列28-埃拉托斯特尼筛法

目录

算法核心思想

[C 语言实现代码及详细注释](#C 语言实现代码及详细注释)

关键点解释

算法可视化


​埃拉托斯特尼筛法(Sieve of Eratosthenes)​ ​是一种非常古老且高效的算法,用于找出一定范围内所有的素数

算法核心思想

算法的核心思想非常巧妙:从 2 开始,将每个​​素数​ ​的​​所有倍数​ ​标记为​​非素数​​(合数)。

  1. ​创建列表​ ​:首先创建一个从 2n的连续整数列表。

  2. ​选取素数​ ​:选取第一个未被标记的数 p(它一定是素数)。当我们找到第一个未被标记的数 p(即 sieve[p]True)时,它一定是素数,因为如果它是合数,它必定有小于 p的质因子。但由于我们从 2 开始逐步标记倍数,任何小于 p的质因子都已经处理过,并标记了 p的倍数(包括 p本身)为合数。因此,如果 p未被标记,说明没有小于 p的质因子能标记它,所以 p是素数。

  3. ​标记倍数​ ​:将 p的所有倍数(2p, 3p, 4p, ...)标记为合数。

  4. ​重复步骤​ ​:重复步骤 2 和 3,直到 p² > n

  5. ​得到结果​​:所有未被标记的数即为素数。


C 语言实现代码及详细注释

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h> // 为了使用 bool 类型

unsigned long long sieve(unsigned long long size) {
    // 1. 动态申请一个布尔数组,用于标记下标对应的数是否为素数
    // sieve[i] 为 true 表示数字 i 是素数
    bool *sieve = (bool*)malloc((size + 1) * sizeof(bool));
    if (sieve == NULL) {
        printf("Memory allocation failed!\n");
        exit(1);
    }

    // 2. 初始化数组,假设所有数初始都是素数
    for (unsigned long long i = 0; i <= size; i++) {
        sieve[i] = true;
    }

    // 0 和 1 不是素数,手动标记为 false
    sieve[0] = false;
    sieve[1] = false;

    // 3. 核心算法:埃拉托斯特尼筛法
    // 只需遍历到 sqrt(size) 即可
    for (unsigned long long i = 2; i * i <= size; i++) {
        // 如果 i 是素数(未被标记为合数)
        if (sieve[i] == true) {
            // 从 i*i 开始,将 i 的所有倍数标记为合数
            // (因为比 i*i 小的倍数已经被更小的素数标记过了)
            for (unsigned long long j = i * i; j <= size; j += i) {
                sieve[j] = false;
            }
        }
    }

    // 4. 统计素数的个数
    unsigned long long count = 0;
    for (unsigned long long i = 2; i <= size; i++) {
        if (sieve[i] == true) {
            count++;
        }
    }

    // 5. 释放动态申请的数组内存
    free(sieve);

    return count;
}

int main() {
    unsigned long long n = 100000000000; // 100亿
    unsigned long long result = sieve(n);
    printf("The number of primes less than %llu is: %llu\n", n, result);
    return 0;
}

注意:编译此代码时需要链接数学库,例如使用 GCC 编译器:gcc -o sieve sieve.c -lm


关键点解释

  1. ​动态内存分配​​:

    • 由于 n可能非常大(如 100 亿),我们不能使用静态数组(如 bool sieve[100000000001]),因为这会在栈上分配内存,导致栈溢出。

    • 使用 malloc在​​堆​​上申请内存,堆的空间远大于栈。

  2. ​优化:外层循环条件 i * i <= size​:

    • 这是一个关键优化。如果一個数 size是合数,那么它一定有一个小于或等于 sqrt(size)的质因数。

    • 因此,我们只需要用 [2, sqrt(size)]范围内的素数去标记它们的倍数,就能确保所有合数都被标记出来。这大大减少了循环次数。

  3. ​优化:内层循环从 i * i开始​​:

    • 对于素数 i,比 i*i小的倍数(如 i*2, i*3, ..., i*(i-1))已经被比 i更小的素数(如 2, 3, ...)标记过了。

    • i*i开始标记避免了大量的重复工作。

  4. ​空间与时间的权衡​​:

    • 该算法是一种典型的"以空间换时间"的算法。我们需要一个大小为 n+1的数组,但换来了 O(n log log n)的时间复杂度,这非常高效。

算法可视化

假设我们要找出 n=20以内的所有素数,其过程如下所示。图表清晰地展示了筛选的每一步,从2开始,依次标记其倍数,直到所有合数都被标记出来。

希望这个详细的解释能帮助你完全理解 C 语言中的埃拉托斯特尼筛法!

相关推荐
地平线开发者20 小时前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮21 小时前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者21 小时前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考21 小时前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx1 天前
CART决策树基本原理
算法·机器学习
Wect1 天前
LeetCode 210. 课程表 II 题解:Kahn算法+DFS 双解法精讲
前端·算法·typescript
颜酱1 天前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法
Gorway1 天前
解析残差网络 (ResNet)
算法
拖拉斯旋风1 天前
LeetCode 经典算法题解析:优先队列与广度优先搜索的巧妙应用
算法
Wect1 天前
LeetCode 207. 课程表:两种解法(BFS+DFS)详细解析
前端·算法·typescript