java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846 |
---|
- 吐槽一下:打表整活的兄弟们,你们知道我们提交后的心情吗?发现前面居然有如此高手写出如此高效的算法,正要好好观摩一下,一看居然是打表操作的时侯的心情吗。(打表:就是看到官方测试用例少,不断提交,此时会提示哪个用例出错,然后针对这个用例直接返回对应答案的操作。)
埃氏筛
解题思路:时间复杂度O( n ∗ l o g 2 l o g 2 n n*log_2{log_2n} n∗log2log2n),空间复杂度O( n n n) |
---|
- 一个素数的倍数一定是合数。例如2的倍数4,6,8…都是合数
- 如果依次按素数从小到大,将其倍数都标识出来,最后会将所有合数都找到
- 最好的是:当我们依照从小到大(从2开始)标识倍数,每次遇到没有被标识的数,就是一个质数。但是前提是你严格按照顺序。
- 有了上面的依据,我们就可以规划算法了
- 初始创建一个数组isPrim[]用来标识当前数字是否是一个质数。如果是就是true。初始我们认为所有数字都是质数
- 从2开始,从小到大,如果当前数字i的isPrim[i] = true,就将倍数都标识出来(false)
- 最后统计true的个数就是质数的个数
优化思路
- 找比n小的质数。需要从小到大枚举到哪里?
sqrt(n),也就是 n \sqrt{n} n,用10举例,10的一半是5,5本身是质数,但是2*5就已经是10了,我们找的是比10小的。因此,最多就遍历到 n \sqrt{n} n就可以规划好所有10以内的数。例如2这个质数可以规划
2*2 = 4,2*3=6,2*4=8
.3这个质数可以规划3*2=6,3*3 = 9
,5这个质数什么都不可以规划了
因此,只需要规划到 n \sqrt{n} n。我们就找到小于n的所有的合数4,6,8,9
- 找质数i的倍数每次都从2开始吗?
2这个质数可以规划
2*2 = 4,2*3=6,2*4=8
,3这个质数可以规划2*3=6,3*3 = 9
.我们发现3*2=6
规划了两次,2规划时1次,3规划时又一次
也就是i这个质数,用比自己小的倍数规划,一定是重复操作。因此从自己本身作为倍数是可以防止这个重复的,也就是i*i开始规划
代码 |
---|
class Solution {
public int countPrimes(int n) {
boolean[] isPrim = new boolean[n];//是否是质数
Arrays.fill(isPrim, true);//初始默认都是质数
// 从 2 开始枚举到 sqrt(n)。
for (int i = 2; i * i < n; i++) {
// 如果当前是素数
if (isPrim[i]) {
// 就把从 i*i 开始,i 的所有倍数都设置为 false。
//因为比i小的都会被之前的数枚举到,例如枚举2的时候,一定将2*2,2*3,2*4,2*4....2*i都枚举了。
//因此我们没必枚举2*i,直接从i*i枚举i*i,i*(i+1),i*(i+2)
for (int j = i * i; j < n; j+=i) {//j = i*i, i*(i+1) = i*i+i = j+i. i*(i+2) = i*i+2i = j+i+i
isPrim[j] = false;//合数设置为false
}
}
}
// 计数,统计质数的个数
int cnt = 0;
for (int i = 2; i < n; i++) {
if (isPrim[i]) {
cnt++;
}
}
return cnt;
}
}
线性筛
埃氏筛的问题在于,虽然我们从i*i这个倍数开始规划合数可以避免比i小的倍数的重复操作。但是比i大的倍数还是会重复。例如
3*15=45,5*9 = 45
而线性筛之所以叫线性筛,是因为它的时间复杂度是线性的 O ( n ) O(n) O(n).也就是它所有元素只会访问一次。例如45只会访问一次。
利用性质:从左到右 ( 2 , + ∞ ) (2,+\infin) (2,+∞)依次,遇到质数x就用<=它的所有质数相乘,加上所有合数 乘 比它小的所有质数,所得到列表。就是质数合数列表
- 两个质数相乘必然为质数
- 一个质数乘以一个合数必然为合数。两个合数相乘也必然为合数。每个合数都可以通过唯一的一个最小质因子得出。(
也就是说,如果一个合数仅仅是通过其最小质因子标识的,那么它只会被访问一次,而不会重复访问。
)从小到大依次用质数标记一个数x时,每个合数的最小质因数都可以最先标记它。也就是一个合数x,从小到大依次取余比它小的质数prime,第一次为0时,prime就是它的最小质因子
解题思路:时间复杂度O( n n n),空间复杂度O( n n n) |
---|
- 多维护一个数组,保存依次找到的素数
- 不再当x为质数时进行标记,而是对每个整数x都进行标记。
- x不再通过倍数进行标记,而是通过质数集合中的数与其相乘进行标记。
每个合数x要遵循被自己的最小质因子标记。这就是优化的关键。
优化思路:如果文字不好理解可以参考这个视频https://www.bilibili.com/video/BV1LR4y1Z7pm
- 如果当前x是合数的话,它会优先被最小质因子prime整除,也就是 x % p r i m e = = 0 x \% prime == 0 x%prime==0.此时需要立即终止对x的继续标记。否则会破坏每个合数被最小质因子标识的规则,就会发生重复访问。
因为合数乘质数一定 = 合数。当x找到其最小质因子prime后,标识
x*prime
,此时依然符合每个合数被最小质因子标识。但如果继续用更大的prime对x进行标识,就会破坏这个规则。
- 例如x = 4时,我们一定已经统计了2和3这两个素数
- 先将
2*4 = 8
标记为合数。此时就立即停止,因为如果我们继续将3*4 = 12
标记为合数的话,12的最小质因子是2,此时它居然通过了3这个质数进行标识。这样就破坏了规则- 当x = 6时,6是合数。我们一定统计了2,3,5这3个素数。我们标识
2*6=12
时,就会发现,它被重复标记了。此时如果继续标记6*3 = 18和6*5 = 30
- 用x = 15时举例,15是合数。我们标识
2*15 = 30
此时发现又和6*5 = 30
重复了,因为5不是30的最小质因子,而2才是30的最小质因子
- 因此,只要x是合数,就只从最小质数2,一直乘到x的最小质因子。
例如15这个合数,只标记
15*2 = 30和15*3 = 45
。因为15的最小质因子是3.这样30和45都保证了被最小质因子所标识的规则,30的最小质因子是2,45的最小质因子是3. 而如果我们继续标识15*5 = 75
,而75的最小质因子是3.就会在通过和数25标识25*3 = 75
时重复标记
代码:因为取余操作比较慢,而且leetcode的判题规则并不完全按照时间复杂度来算,因此这个的做题效率反而不如埃氏筛。但是如果n足够大,线性筛的效率也会越来越优于埃氏筛。但是埃氏筛更通用一点 |
---|
class Solution {
public int countPrimes(int n) {
int primes[] = new int[n/2];//保存质数
int length = 0;
int[] isPrime = new int[n];
// Arrays.fill(isPrime, 1);//如果 规定 isPrime[i] == 0为质数,就可以省略这个操作。
for (int i = 2; i < n; ++i) {
if (isPrime[i] == 0) {//如果当前i是质数
primes[length++] = i;//将其记录到质数数组中
}
for (int j = 0; j < length && i * primes[j] < n; ++j) {
isPrime[i * primes[j]] = 1;//标记合数,两个质数相乘 或者 一个质数*一个合数 都为合数
if (i % primes[j] == 0) {//如果这个条件成立,说明当前质数primes[j]是合数i的最小质因子,
break;//立即终止,否则会破坏每个合数都是通过最小质因子发现的规则
}
}
}
return length;
}
}
质数性质+奇偶性质+埃氏筛(卷!卷!卷卷卷卷卷卷!)
因为兄弟们越来越卷,搞的上面两种算法的效率都被压下去了,因此我们针对两者的优点和缺点进行优化
- 埃氏筛的缺点是具有重复操作合数的问题,例如10这个合数,会被2和5两个质数所处理
- 线性筛虽然解决了重复操作的问题,但是每次都需要取余操作判断是否是合数,这个就慢了
解题思路:时间复杂度O( n ∗ l o g 2 l o g 2 n n*log_2{log_2n} n∗log2log2n),空间复杂度O( n n n) |
---|
理论时间复杂度和经典的埃氏筛一样,但是实际时间复杂度达到了O(n),而且是没有取余操作的O(n).因为这个算法将重复操作都直接跳过了
- 使用埃氏筛的基础上,利用如下性质
- 质数中,只有一个偶数就是2,其它都是奇数.因此对于小于n的所有质数,最多只有 n 2 \dfrac{n}{2} 2n,也就是最多是n的一半,因为除了2以外的所有偶数都不是质数,直接排除即可。
- 每个质数将它的倍数都剔除之后,下一个遇到的奇数一定是质数。
这个很关键,我们顺序遍历数字时,通过3这个质数,会将
3*3 = 9
这个数标为合数,因此下一个奇数就是5,一定是质数,5*5 = 25
也标为合数,然后是7一定是质数,7*7 = 49
也是合数。下一个奇数是9我们已经表示为合数,下一个是11一定是质数。
- 一个质数 乘 一个大于1的奇数,必然为合数。
例如3是质数,则大于1的奇数为3,5,7,9…因此
3*3
和3*5
和3*7
和3*9
…都是合数
- 因此我们可以将找素数变成找合数。初始因为除了2以外的偶数都是合数,因此我们将剩下的奇数和2都看作是质数。因此初始我们有
count = n/2个质数
。而我们的dp数组也由保存是否是质数,变成是否是合数。true表示当前i是合数。所以初始dp数组全部是false,表示都是质数- 接下来,我们要跳过所有偶数,因为2以外的偶数都是合数,只遍历奇数。也就是从3开始遍历,每次+2.也就是3,5,7,9,11…
- 然后根据性质2:每个质数将它的倍数(合数)都剔除之后,下一个遇到的奇数一定是质数。
我们每访问一个奇数都要查看它是否是被剔除后的数(合数),如果是就跳过。这样下一个没有被剔除的奇数一定是下一个质数
- 当拿到质数后,我们要通过性质3:一个质数 乘 一个大于1的奇数,必然为合数,将所有合数标识出来(true),并且每次都count-1.因为我们初始有count个质数(抛弃所有>2的偶数),现在又找到了新的合数(且是奇数),那么count就要-1.
这里标识的true就是第4步需要用的,跳过被剔除的合数
代码:前面效率比这个算法高的,都是打表整活的(根本不能算作算法),还有几个是以前测试用例少的时候提交的代码,他们的代码放到现在提交都到170ms开外了 |
---|
class Solution {
public int countPrimes(int n) {
//dp数组,当前数(下标)是否是合数,true表示为合数,默认都是质数(false)
boolean notPrime[] = new boolean[n];
//如果n小于3的话就没有 因为找到n以内 所以2以内是没有的
if (n < 3) return 0;
//除了2以外的所有的偶数都不是素数,所有我们可以剔除一半,最多情况下,小于n的所有奇数都是质数
int count = n / 2;
// 之后我们只要剔除 在这个奇数范围内 不是素数的数就可以了
// 因为我们已经把偶数去掉了,所以只要剔除奇数的奇数倍就可以了
for (int i = 3; i * i < n; i += 2) {//第一个奇数是3,i+=2正好是下一个奇数
//如果i已经标为合数就跳过,避免重复遍历
if (notPrime[i]) continue;
//否则i必然是质数,因为一个质数和一个大于1的奇数相乘必然为合数。
//为了避免冗余,小于i的奇数会被前面的质数处理,因此我们从其本身这个奇数开始乘,也就是j = i*i。例如j = 3*3 = 9
//之后的奇数都是不断的进行+2操作,例如3+2 = 5,5+2 = 7 实现i*5,i*7,i*9,i*11.
//也就是i*(i+2) = i * i + i+2. 而j = i*i,因此接下来不断让 j + i*2 ,
//也就是i = 3为质数, 让其不断乘以大于等于i的奇数。例如j = 3*3 = 9, j = 3*5 = 15, j = 3*7 = 21, j = 3 * 9 = 27,这些都是合数
for (int j = i * i; j < n; j += i * 2) {
if (!notPrime[j]) {//如果当前这个合数还没有访问过
notPrime[j] = true;//将其标志为合数
count --;//则质数个数-1
}
}
}
return count;
}
}