打印素数.md 10.2 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '如何高效寻找素数'
L
labuladong 已提交
3
tags: ['数组', '数学']
L
labuladong 已提交
4
---
L
labuladong 已提交
5

6 7
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
8
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
9 10 11 12
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
13
![](https://labuladong.github.io/pictures/souyisou1.png)
14

L
labuladong 已提交
15
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
16 17


L
labuladong 已提交
18 19 20 21 22 23

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [204. Count Primes](https://leetcode.com/problems/count-primes/) | [204. 计数质数](https://leetcode.cn/problems/count-primes/) | 🟠
24 25 26

**-----------**

L
labuladong 已提交
27 28
素数的定义看起来很简单,如果一个数如果只能被 1 和它本身整除,那么这个数就是素数。

L
labuladong 已提交
29 30 31
虽然素数的定义并不复杂,恐怕没多少人真的能把素数相关的算法写得高效。

比如力扣第 204 题「计数质数」,让你写这样一个函数:
L
labuladong 已提交
32

L
labuladong 已提交
33
<!-- muliti_language -->
L
labuladong 已提交
34 35 36 37 38 39 40 41 42 43
```java
// 返回区间 [2, n) 中有几个素数 
int countPrimes(int n)

// 比如 countPrimes(10) 返回 4
// 因为 2,3,5,7 是素数
```

你会如何写这个函数?我想大家应该会这样写:

L
labuladong 已提交
44
<!-- muliti_language -->
L
labuladong 已提交
45 46 47 48
```java
int countPrimes(int n) {
    int count = 0;
    for (int i = 2; i < n; i++)
L
labuladong 已提交
49
        if (isPrime(i)) count++;
L
labuladong 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
    return count;
}

// 判断整数 n 是否是素数
boolean isPrime(int n) {
    for (int i = 2; i < n; i++)
        if (n % i == 0)
            // 有其他整除因子
            return false;
    return true;
}
```

这样写的话时间复杂度 O(n^2),问题很大。**首先你用 isPrime 函数来辅助的思路就不够高效;而且就算你要用 isPrime 函数,这样写算法也是存在计算冗余的**

L
labuladong 已提交
65
先来简单说下**如果你要判断一个数是不是素数,应该如何写算法**。只需稍微修改一下上面的 isPrime 代码中的 for 循环条件:
L
labuladong 已提交
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91

```java
boolean isPrime(int n) {
    for (int i = 2; i * i <= n; i++)
        ...
}
```

换句话说,`i` 不需要遍历到 `n`,而只需要到 `sqrt(n)` 即可。为什么呢,我们举个例子,假设 `n = 12`

```java
12 = 2 × 6
12 = 3 × 4
12 = sqrt(12) × sqrt(12)
12 = 4 × 3
12 = 6 × 2
```

可以看到,后两个乘积就是前面两个反过来,反转临界点就在 `sqrt(n)`

换句话说,如果在 `[2,sqrt(n)]` 这个区间之内没有发现可整除因子,就可以直接断定 `n` 是素数了,因为在区间 `[sqrt(n),n]` 也一定不会发现可整除因子。

现在,`isPrime` 函数的时间复杂度降为 O(sqrt(N)),**但是我们实现 `countPrimes` 函数其实并不需要这个函数**,以上只是希望读者明白 `sqrt(n)` 的含义,因为等会还会用到。

### 高效实现 `countPrimes`

L
labuladong 已提交
92 93 94
接下来介绍的方法叫做「素数筛选法」,这个方法是古希腊一位名叫埃拉托色尼的大佬发明的,我们在中学的教课书上见过他的大名,因为他就是第一个通过物体的影子正确计算地球周长的人,被推崇为「地理学之父」。

回到正题,素数筛选法的核心思路是和上面的常规思路反着来:
L
labuladong 已提交
95 96 97 98 99

首先从 2 开始,我们知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8... 都不可能是素数了。

然后我们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12... 也都不可能是素数了。

L
labuladong 已提交
100 101
Wikipedia 的这个 GIF 很形象:

L
labuladong 已提交
102
![](https://labuladong.github.io/pictures/prime/1.gif)
L
labuladong 已提交
103

L
labuladong 已提交
104 105
看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码:

L
labuladong 已提交
106
<!-- muliti_language -->
L
labuladong 已提交
107 108
```java
int countPrimes(int n) {
L
labuladong 已提交
109
    boolean[] isPrime = new boolean[n];
L
labuladong 已提交
110
    // 将数组都初始化为 true
L
labuladong 已提交
111
    Arrays.fill(isPrime, true);
L
labuladong 已提交
112 113

    for (int i = 2; i < n; i++) 
L
labuladong 已提交
114
        if (isPrime[i]) 
L
labuladong 已提交
115 116
            // i 的倍数不可能是素数了
            for (int j = 2 * i; j < n; j += i) 
L
labuladong 已提交
117
                    isPrime[j] = false;
L
labuladong 已提交
118 119 120
    
    int count = 0;
    for (int i = 2; i < n; i++)
L
labuladong 已提交
121
        if (isPrime[i]) count++;
L
labuladong 已提交
122 123 124 125 126 127 128 129 130 131 132
    
    return count;
}
```

如果上面这段代码你能够理解,那么你已经掌握了整体思路,但是还有两个细微的地方可以优化。

首先,回想刚才判断一个数是否是素数的 `isPrime` 函数,由于因子的对称性,其中的 for 循环只需要遍历 `[2,sqrt(n)]` 就够了。这里也是类似的,我们外层的 for 循环也只需要遍历到 `sqrt(n)`

```java
for (int i = 2; i * i < n; i++) 
L
labuladong 已提交
133
    if (isPrime[i]) 
L
labuladong 已提交
134 135 136 137 138 139 140
        ...
```

除此之外,很难注意到内层的 for 循环也可以优化。我们之前的做法是:

```java
for (int j = 2 * i; j < n; j += i) 
L
labuladong 已提交
141
    isPrime[j] = false;
L
labuladong 已提交
142 143 144 145
```

这样可以把 `i` 的整数倍都标记为 `false`,但是仍然存在计算冗余。

L
labuladong 已提交
146
比如 `n = 25``i = 5` 时算法会标记 5 × 2 = 10,5 × 3 = 15 等等数字,但是这两个数字已经被 `i = 2``i = 3` 的 2 × 5 和 3 × 5 标记了。
L
labuladong 已提交
147 148 149 150 151

我们可以稍微优化一下,让 `j``i` 的平方开始遍历,而不是从 `2 * i` 开始:

```java
for (int j = i * i; j < n; j += i) 
L
labuladong 已提交
152
    isPrime[j] = false;
L
labuladong 已提交
153 154 155 156
```

这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码:

L
labuladong 已提交
157
<!-- muliti_language -->
L
labuladong 已提交
158
```java
L
labuladong 已提交
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
class Solution {
    public int countPrimes(int n) {
        boolean[] isPrime = new boolean[n];
        Arrays.fill(isPrime, true);
        for (int i = 2; i * i < n; i++) 
            if (isPrime[i]) 
                for (int j = i * i; j < n; j += i) 
                    isPrime[j] = false;
        
        int count = 0;
        for (int i = 2; i < n; i++)
            if (isPrime[i]) count++;
        
        return count;
    }
L
labuladong 已提交
174
}
L
labuladong 已提交
175

L
labuladong 已提交
176 177 178 179 180 181 182 183 184 185 186
```

**该算法的时间复杂度比较难算**,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是:

  n/2 + n/3 + n/5 + n/7 + ...
= n × (1/2 + 1/3 + 1/5 + 1/7...)

括号中是素数的倒数。其最终结果是 O(N * loglogN),有兴趣的读者可以查一下该算法的时间复杂度证明。

以上就是素数算法相关的全部内容。怎么样,是不是看似简单的问题却有不少细节可以打磨呀?

L
labuladong 已提交
187 188 189 190 191 192 193


<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [丑数系列算法详解](https://labuladong.github.io/article/fname.html?fname=丑数)
L
labuladong 已提交
194
 - [配套 Chrome 刷题插件](https://labuladong.github.io/article/fname.html?fname=chrome插件简介)
L
labuladong 已提交
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209

</details><hr>




<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>

<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>

| LeetCode | 力扣 |
| :----: | :----: |
| [264. Ugly Number II](https://leetcode.com/problems/ugly-number-ii/?show=1) | [264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/?show=1) |
L
labuladong 已提交
210
| - | [剑指 Offer 49. 丑数](https://leetcode.cn/problems/chou-shu-lcof/?show=1) |
L
labuladong 已提交
211 212 213 214 215

</details>



216
**_____________**
何睿 已提交
217

L
labuladong 已提交
218
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
219

L
labuladong 已提交
220
![](https://labuladong.github.io/pictures/souyisou2.png)
L
labuladong 已提交
221 222


罗巍耀-LWW 已提交
223 224
======其他语言代码======

B
brucecat 已提交
225 226 227
[204.计数质数](https://leetcode-cn.com/problems/count-primes)

### C++
罗巍耀-LWW 已提交
228 229 230 231 232 233 234 235
采用的算法是埃拉托斯特尼筛法
埃拉托斯特尼筛法的具体内容就是:**要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。**
同时考虑到大于2的偶数都不是素数,所以可以进一步优化成:**要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的奇数倍剔除,剩下的奇数就是素数。**
此算法其实就是上面的Java解法所采用的。

这里提供C++的代码:
```C++
class Solution {
B
brucecat 已提交
236 237
  public:
  int countPrimes(int n) {
罗巍耀-LWW 已提交
238 239 240
    int res = 0;
    bool prime[n+1];
    for(int i = 0; i < n; ++i)
B
brucecat 已提交
241
      prime[i] = true;
罗巍耀-LWW 已提交
242 243 244

    for(int i = 2; i <= sqrt(n); ++i)   //计数过程 
    {                                   //外循环优化,因为判断一个数是否为质数只需要整除到sqrt(n),反推亦然
B
brucecat 已提交
245 246 247
      if(prime[i])
      {
        for(int j = i * i; j < n; j += i)   //内循环优化,i*i之前的比如i*2,i*3等,在之前的循环中已经验证了
罗巍耀-LWW 已提交
248
        {
B
brucecat 已提交
249 250 251
          prime[j] = false;
        }
      }      
罗巍耀-LWW 已提交
252 253
    }
    for (int i = 2; i < n; ++i)
B
brucecat 已提交
254
      if (prime[i])  res++;     //最后遍历统计一遍,存入res
罗巍耀-LWW 已提交
255 256

    return res;    
B
brucecat 已提交
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
  }
};
```



### javascript

```js
var countPrimes = function (n) {
    let isPrime = new Array(n);
    isPrime.fill(true, 0, n)

    // 计数过程
    // 外循环优化,因为判断一个数是否为质数只需要整除到sqrt(n),反推亦然
    for (let i = 2; i * i < n; i++) {
        if (isPrime[i]) {
            // 内循环优化,i*i之前的比如i*2,i*3等,在之前的循环中已经验证了
            for (let j = i * i; j < n; j += i) {
                isPrime[j] = false;
            }
        }
    }

    let res = 0;
    for (let i = 2; i < n; i++) {
        //最后遍历统计一遍,存入res
        if (isPrime[i]) res++;
罗巍耀-LWW 已提交
285
    }
B
brucecat 已提交
286
    return res;
罗巍耀-LWW 已提交
287 288 289 290 291
};
```