給定 N,求區間 [2, N] 內所有質數。質數:大於 1 且除 1 和自身外無其他因數的自然數。
| 版本 | 名稱 | 時間複雜度 | 空間 | 核心優化 |
|---|---|---|---|---|
| v1 | 試除法 | O(N√N) | O(1) | 無 |
| v2 | 優化試除 (只試質數) | O(N log log N) 近似 | O(π(N)) | 已找質數作除數 |
| v3 | 埃拉托斯特尼篩 (基礎) | O(N log log N) | O(N) | 標記倍數而非除法 |
| v4 | 優化篩 (只存奇數) | O(N log log N) | O(N/2) | 空間減半,跳過偶數 |
| v5 | 線性篩 (歐拉篩) | O(N) | O(N) | 每個合數只被最小質因數標記一次 |
void sieve_eratosthenes(int n, int **primes_out, int *count_out) {
if (n < 2) { *count_out = 0; *primes_out = NULL; return; }
char *is_composite = calloc(n + 1, 1); // 0: prime, 1: composite
int *primes = malloc((n / 2 + 1) * sizeof(int)); // 上界
int count = 0;
for (int i = 2; i <= n; ++i) {
if (!is_composite[i]) {
primes[count++] = i;
if ((long long)i * i <= n) // 防溢出
for (int j = i * i; j <= n; j += i)
is_composite[j] = 1;
}
}
*primes_out = realloc(primes, count * sizeof(int));
*count_out = count;
free(is_composite);
}
void sieve_odd_only(int n, int **primes_out, int *count_out) {
if (n < 2) { *count_out = 0; *primes_out = NULL; return; }
int size = (n - 1) / 2; // 只存 3,5,7... 對應索引 i -> 數值 2*i+3
char *is_comp = calloc(size, 1);
int *primes = malloc((n / 2 + 1) * sizeof(int));
int count = 0;
primes[count++] = 2; // 2 是唯一偶質數
for (int i = 0; i < size; ++i) {
if (!is_comp[i]) {
int p = 2*i + 3;
primes[count++] = p;
// 從 p*p 開始標記,步長 2p (保持奇數)
long long start = 1LL * p * p;
if (start > n) continue;
for (long long j = start; j <= n; j += 2LL * p)
is_comp[(j - 3) / 2] = 1;
}
}
*primes_out = realloc(primes, count * sizeof(int));
*count_out = count;
free(is_comp);
}
void sieve_linear(int n, int **primes_out, int *count_out) {
if (n < 2) { *count_out = 0; *primes_out = NULL; return; }
char *is_comp = calloc(n + 1, 1);
int *primes = malloc((n / 2 + 1) * sizeof(int));
int count = 0;
for (int i = 2; i <= n; ++i) {
if (!is_comp[i]) primes[count++] = i;
for (int j = 0; j < count && 1LL * i * primes[j] <= n; ++j) {
is_comp[i * primes[j]] = 1;
if (i % primes[j] == 0) break; // 核心:保證每個合數只被最小質因數標記
}
}
*primes_out = realloc(primes, count * sizeof(int));
*count_out = count;
free(is_comp);
}
每個合數 x = i * p (p 為質數) 被標記的條件:
p 是 x 的最小質因數 (因為外層 i 從小到大,內層 p 從小到大)。p 整除 i 時 break,阻止更大的質數去標記 i * p (那時最小質因數會變成更大的質數,違反唯一性)。因此每個合數**恰好被標記一次**,總操作數 ∑ (合數個數) = O(N)。
| 算法 | 耗時 (ms) | 質數個數 | 記憶體 (MB) |
|---|---|---|---|
| 試除法 | ~4500 | 664,579 | <1 |
| 埃氏篩 (基礎) | ~120 | 664,579 | 10 |
| 奇數優化篩 | ~65 | 664,579 | 5 |
| 線性篩 | ~85 | 664,579 | 10 |
註:實際工程中 奇數優化篩常比線性篩更快** (常數因子小、緩存友好、無分支預測失誤),線性篩優勢在極大 N (10^8+) 或需最小質因數分解時顯現。
uint8_t 每位存一個奇數,空間再減 8 倍 (N=10^9 需 ~60MB)。φ(30)/30 = 4/15。const int small_primes[] = {2,3,5,7,11,13,17,19,23,29}; 手工展開前幾輪。| 場景 | 推薦算法 | 理由 |
|---|---|---|
| N ≤ 10^7、單次查詢、教學/競賽 | 奇數優化埃氏篩 | 代碼短、常數小、極快、易理解 |
| N ≤ 10^8、需最小質因數分解/歐拉函數 | 線性篩 | 同步獲得最小質因數 lp[x] |
| N > 10^8、內存受限 | 分段篩 + 位壓縮 | 空間 O(√N),可處理 10^12+ |
| 多次查詢、預計算 | 離線篩 + 存儲質數表 | 查詢 O(1) 二分/前綴和 |
| 嵌入式/無堆內存 | 靜態位圖 + 奇數優化 | 無 malloc,確定性內存 |
#include#include #include #include // 選擇一個實現 void sieve(int n, int **primes, int *count); // 實現見上文 int main() { int n = 10000000; int *primes; int count; clock_t t = clock(); sieve(n, &primes, &count); t = clock() - t; printf("N=%d, primes=%d, time=%.3f ms\n", n, count, (double)t * 1000 / CLOCKS_PER_SEC); // 輸出前 10 個驗證 for (int i = 0; i < 10 && i < count; ++i) printf("%d ", primes[i]); puts(""); free(primes); return 0; }
從 O(N√N) 試除到 O(N) 線性篩,質數篩選算法展示了數論洞察 (最小質因數唯一性) 如何轉化為極致時間複雜度。工程實踐中,**奇數優化埃氏篩**以代碼簡潔、常數極小、緩存友好,成為 10^7 級以下首選;**線性篩**在需最小質因數分解、極大 N 或理論研究中不可替代。掌握兩者核心邏輯與優化技巧,是演算法工程師的基本功。