【C语言】统计孪生素数对高效解法
使用埃拉托斯特尼筛法(Sieve of Eratosthenes) 的高效解法,时间复杂度为 O (n log log n),显著优于逐个判断素数的 O (n²) 复杂度:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>int count_prime_pairs(int n) {if (n < 5) return 0; // 小于5时无孪生素数对// 创建布尔数组标记素数,初始全为true(假设都是素数)bool* is_prime = (bool*)calloc(n + 1, sizeof(bool));if (!is_prime) return -1; // 内存分配失败// 初始化:0和1不是素数,其余默认是素数for (int i = 2; i <= n; i++) is_prime[i] = true;// 埃拉托斯特尼筛法:标记非素数for (int i = 2; i * i <= n; i++) {if (is_prime[i]) {for (int j = i * i; j <= n; j += i) {is_prime[j] = false;}}}// 统计孪生素数对int count = 0;for (int p = 3; p <= n - 2; p += 2) { // 从3开始只检查奇数if (is_prime[p] && is_prime[p + 2]) {count++;}}free(is_prime); // 释放内存return count;
}int main() {int n = 10;printf("孪生素数对数量:%d\n", count_prime_pairs(n)); // 输出:2return 0;
}
核心优化点
-
筛法求素数:
- 一次性标记所有非素数,避免重复判断。
- 时间复杂度 O (n log log n),接近线性时间。
-
内存优化:
- 使用布尔数组替代整数数组,节省空间。
- 动态分配内存适应不同的 n 值。
-
奇数优化:
- 孪生素数对中,p 和 q 必须都是奇数(除了 (3,5) 外,其他偶数不可能是素数)。
- 循环步长设为 2,跳过偶数检查。
-
边界处理:
- 当 n < 5 时直接返回 0,减少不必要计算。
代码核心理解
1. bool* is_prime = (bool*)calloc(n + 1, sizeof(bool));
的语法解释
这行代码使用了 C 语言的动态内存分配函数 calloc
,其作用是在程序运行时动态分配一块连续的内存空间,并初始化为 0。具体解析如下:
-
calloc
函数原型:void* calloc(size_t num_elements, size_t element_size);
num_elements
:需要分配的元素数量(这里是n + 1
,表示从 0 到 n 的所有索引)element_size
:每个元素的大小(这里是sizeof(bool)
,通常为 1 字节)- 返回值:指向分配内存的指针(类型为
void*
,需要强制转换为目标类型)
-
强制类型转换
(bool*)
:将void*
类型的指针转换为bool*
类型,以便赋值给is_prime
变量。 -
内存初始化:
calloc
会自动将分配的内存全部初始化为 0(即每个bool
值都为false
),这与malloc
不同(malloc
分配的内存值是未定义的)。
2. 埃拉托斯特尼筛法(Sieve of Eratosthenes)的逻辑
埃拉托斯特尼筛法是一种高效的素数筛选算法,其核心思想是:从 2 开始,将每个素数的倍数标记为合数。具体步骤如下:
// 初始化:0和1不是素数,其余默认是素数
for (int i = 2; i <= n; i++) is_prime[i] = true;// 埃拉托斯特尼筛法:标记非素数
for (int i = 2; i * i <= n; i++) {if (is_prime[i]) {for (int j = i * i; j <= n; j += i) {is_prime[j] = false;}}
}
-
初始化阶段:创建一个布尔数组
is_prime
,将所有索引(2 到 n)初始化为true
,表示默认都是素数。 -
筛选阶段:
- 外层循环:从 2 开始遍历到
√n
(因为如果一个数n
不是素数,它一定有一个因子小于等于√n
)。 - 检查当前数
i
:如果is_prime[i]
仍为true
,说明i
是素数(未被前面的步骤标记为合数)。 - 标记合数:将
i
的所有倍数(从i*i
开始,因为更小的倍数已经被之前的素数标记过)标记为false
。
- 外层循环:从 2 开始遍历到
-
优化点:
- 从
i*i
开始:例如,当i=5
时,直接从25
开始标记,因为5*2=10
、5*3=15
、5*4=20
已经被 2 和 3 标记过。 - 只遍历到
√n
:超过√n
的数的倍数已经被更小的素数标记过。
- 从
3. free(is_prime);
的作用:为什么要释放内存?
在 C 语言中,使用 malloc
、calloc
或 realloc
动态分配的内存必须在不再使用时手动释放,否则会导致内存泄漏(Memory Leak)。具体原因如下:
-
内存生命周期:动态分配的内存位于堆(Heap)区,不会像栈(Stack)上的局部变量那样在函数返回时自动释放。如果不手动释放,这块内存会一直被占用,直到程序结束。
-
内存泄漏后果:反复分配内存而不释放,会导致程序占用的内存不断增长,最终耗尽系统资源,导致程序崩溃或系统变慢。
-
正确做法:
- 使用
calloc
分配内存。 - 使用完内存后(通常在不再需要该内存的地方,如函数结束前),调用
free
释放它。 - 将指针置为
NULL
(可选),防止误操作:is_prime = NULL;
- 使用
-
类比:就像借书需要归还图书馆一样,动态分配的内存也需要归还给系统,以便其他程序或本程序的其他部分可以使用。