一位同事刚刚告诉我,由于与哈希相关的不可思议的原因,C#词典集合将按质数调整大小。我的直接问题是:“它怎么知道下一个素数?它们是在大型表中还是在运行中进行计算?这是在插入时导致确定大小的可怕的不确定性运行时”
所以我的问题是,给定N(一个质数),计算下一个质数的最有效方法是什么?
一位同事刚刚告诉我,由于与哈希相关的不可思议的原因,C#词典集合将按质数调整大小。我的直接问题是:“它怎么知道下一个素数?它们是在大型表中还是在运行中进行计算?这是在插入时导致确定大小的可怕的不确定性运行时”
所以我的问题是,给定N(一个质数),计算下一个质数的最有效方法是什么?
Answers:
大约一年前,我在为c ++ 11实现无序(哈希)容器时为libc ++工作。我以为我会在这里分享我的经验。这种经验支持marcog对“蛮力”的 合理定义的公认答案。
这意味着在大多数情况下,即使是简单的蛮力也足够快,平均取O(ln(p)* sqrt(p))。
我开发了几种实现size_t next_prime(size_t n)
此功能的规范的实现:
返回: 大于或等于的最小素数
n
。
的每个实现next_prime
都带有辅助功能is_prime
。 is_prime
应被视为私人实施细节;不能直接由客户调用。这些实现中的每一个当然都经过了正确性测试,但还通过以下性能测试进行了测试:
int main()
{
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::duration<double, std::milli> ms;
Clock::time_point t0 = Clock::now();
std::size_t n = 100000000;
std::size_t e = 100000;
for (std::size_t i = 0; i < e; ++i)
n = next_prime(n+1);
Clock::time_point t1 = Clock::now();
std::cout << e/ms(t1-t0).count() << " primes/millisecond\n";
return n;
}
我应该强调,这是一项性能测试,并不反映典型的用法,看起来更像是:
// Overflow checking not shown for clarity purposes
n = next_prime(2*n + 1);
所有性能测试均使用以下命令进行编译:
clang++ -stdlib=libc++ -O3 main.cpp
实施1
有七个实现。显示第一个实现的目的是证明,如果您没有停止测试候选素数x
的因子,sqrt(x)
那么您甚至都无法实现可以归为蛮力的实现。此实现
速度很慢。
bool
is_prime(std::size_t x)
{
if (x < 2)
return false;
for (std::size_t i = 2; i < x; ++i)
{
if (x % i == 0)
return false;
}
return true;
}
std::size_t
next_prime(std::size_t x)
{
for (; !is_prime(x); ++x)
;
return x;
}
对于此实现e
,仅为了获得合理的运行时间,我必须将其设置为100而不是100000:
0.0015282 primes/millisecond
实施2
此实现是蛮力实现中最慢的,并且与实现1的唯一区别是,当因子超过时,它将停止测试素数sqrt(x)
。
bool
is_prime(std::size_t x)
{
if (x < 2)
return false;
for (std::size_t i = 2; true; ++i)
{
std::size_t q = x / i;
if (q < i)
return true;
if (x % i == 0)
return false;
}
return true;
}
std::size_t
next_prime(std::size_t x)
{
for (; !is_prime(x); ++x)
;
return x;
}
请注意,sqrt(x)
它不是直接计算出来的,而是由推断出来的q < i
。这样可以使速度加快数千倍:
5.98576 primes/millisecond
并验证了marcog的预测:
...这完全在大多数问题的约束范围内,而大多数现代硬件上的问题大约需要毫秒。
实施3
通过避免使用%
运算符,可以使速度提高近一倍(至少在我使用的硬件上):
bool
is_prime(std::size_t x)
{
if (x < 2)
return false;
for (std::size_t i = 2; true; ++i)
{
std::size_t q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
}
return true;
}
std::size_t
next_prime(std::size_t x)
{
for (; !is_prime(x); ++x)
;
return x;
}
11.0512 primes/millisecond
实施4
到目前为止,我什至没有使用常识,即2是唯一的偶数素数。此实现结合了这些知识,速度几乎翻了一番:
bool
is_prime(std::size_t x)
{
for (std::size_t i = 3; true; i += 2)
{
std::size_t q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
}
return true;
}
std::size_t
next_prime(std::size_t x)
{
if (x <= 2)
return 2;
if (!(x & 1))
++x;
for (; !is_prime(x); x += 2)
;
return x;
}
21.9846 primes/millisecond
实施4可能是大多数人认为“蛮力”的想法。
实施5
使用以下公式,您可以轻松选择所有不能被2或3整除的数字。
6 * k + {1, 5}
其中k> =1。以下实现使用此公式,但使用可爱的xor技巧实现:
bool
is_prime(std::size_t x)
{
std::size_t o = 4;
for (std::size_t i = 5; true; i += o)
{
std::size_t q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
o ^= 6;
}
return true;
}
std::size_t
next_prime(std::size_t x)
{
switch (x)
{
case 0:
case 1:
case 2:
return 2;
case 3:
return 3;
case 4:
case 5:
return 5;
}
std::size_t k = x / 6;
std::size_t i = x - 6 * k;
std::size_t o = i < 2 ? 1 : 5;
x = 6 * k + o;
for (i = (3 + o) / 2; !is_prime(x); x += i)
i ^= 6;
return x;
}
这实际上意味着该算法只需要检查1/3的整数的素数,而不是数字的1/2,性能测试表明预期的速度提高了近50%:
32.6167 primes/millisecond
实施6
此实现是实现5的逻辑扩展:它使用以下公式计算不能被2、3和5整除的所有数字:
30 * k + {1, 7, 11, 13, 17, 19, 23, 29}
它还展开is_prime内的内部循环,并创建一个“小素数”列表,该列表对于处理小于30的数字很有用。
static const std::size_t small_primes[] =
{
2,
3,
5,
7,
11,
13,
17,
19,
23,
29
};
static const std::size_t indices[] =
{
1,
7,
11,
13,
17,
19,
23,
29
};
bool
is_prime(std::size_t x)
{
const size_t N = sizeof(small_primes) / sizeof(small_primes[0]);
for (std::size_t i = 3; i < N; ++i)
{
const std::size_t p = small_primes[i];
const std::size_t q = x / p;
if (q < p)
return true;
if (x == q * p)
return false;
}
for (std::size_t i = 31; true;)
{
std::size_t q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 6;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 4;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 2;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 4;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 2;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 4;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 6;
q = x / i;
if (q < i)
return true;
if (x == q * i)
return false;
i += 2;
}
return true;
}
std::size_t
next_prime(std::size_t n)
{
const size_t L = 30;
const size_t N = sizeof(small_primes) / sizeof(small_primes[0]);
// If n is small enough, search in small_primes
if (n <= small_primes[N-1])
return *std::lower_bound(small_primes, small_primes + N, n);
// Else n > largest small_primes
// Start searching list of potential primes: L * k0 + indices[in]
const size_t M = sizeof(indices) / sizeof(indices[0]);
// Select first potential prime >= n
// Known a-priori n >= L
size_t k0 = n / L;
size_t in = std::lower_bound(indices, indices + M, n - k0 * L) - indices;
n = L * k0 + indices[in];
while (!is_prime(n))
{
if (++in == M)
{
++k0;
in = 0;
}
n = L * k0 + indices[in];
}
return n;
}
可以说,这超出了“蛮力”,并且有利于将速度再提高27.5%,从而达到:
41.6026 primes/millisecond
实施7
反复玩上面的游戏是很实际的,为不能被2、3、5和7整除的数字建立一个公式:
210 * k + {1, 11, ...},
源代码未在此处显示,但与实现6非常相似。这是我选择实际用于libc ++的无序容器的实现,并且该源代码是开源的(可在链接中找到)。
最后的迭代有利于将速度进一步提高14.6%,以:
47.685 primes/millisecond
使用此算法可确保libc ++哈希表的客户端可以选择自己决定的任何素数,这对他们的情况最有利,并且此应用程序的性能是可以接受的。
return false
情况更有可能,因此仅出于这个原因就值得切换(提速1%)。
以防万一有人好奇:
使用反射器,我确定.Net使用静态类,其中包含〜72个素数的硬编码列表,范围最大为7199369,它扫描的最小素数至少是当前大小的两倍,并且扫描的大小大于它计算出的素数。通过所有奇数的尝试除法得到下一个素数,直至潜在数的平方根。此类是不可变的,并且是线程安全的(即,不会存储较大的质数以备将来使用)。
一个不错的技巧是使用部分筛子。例如,数字N = 2534536543556之后的下一个素数是什么?
相对于一系列小的素数检查N的模数。从而...
mod(2534536543556,[3 5 7 11 13 17 19 23 29 31 37])
ans =
2 1 3 6 4 1 3 4 22 16 25
我们知道N之后的下一个素数必须是奇数,我们可以立即丢弃此小素数列表的所有奇数倍。这些模数使我们能够筛选出这些小质数的倍数。如果我们使用不超过200的小质数,我们可以使用此方案立即丢弃大多数大于N的潜在质数,除了一个小列表。
更明确地说,如果我们要寻找一个超出2534536543556的质数,则不能将其除以2,因此我们只需要考虑超出该值的奇数。上面的模数显示2534536543556与2 mod 3是一致的,因此2534536543556 + 1与0 mod 3是一致的,必须是2534536543556 + 7、2534536543556 + 13等。有效地,我们可以筛选出许多数字而无需任何操作测试它们的原始性,无需进行任何试验划分。
同样,事实是
mod(2534536543556,7) = 3
告诉我们2534536543556 + 4等于0 mod7。当然,这个数字是偶数,因此我们可以忽略它。但是2534536543556 + 11是一个可被7整除的奇数,例如2534536543556 + 25等。同样,我们可以将这些数字排除为清楚的复合数字(因为它们可以被7整除),因此不能以质数表示。
仅使用一小部分最多为37的质数,我们可以排除紧随起始点2534536543556之后的大多数数字,除了少数几个:
{2534536543573 , 2534536543579 , 2534536543597}
在这些数字中,它们是素数吗?
2534536543573 = 1430239 * 1772107
2534536543579 = 99833 * 25387763
我已尽力提供列表中前两个数字的素因式分解。看到它们是复合的,但主要因素很大。当然,这是有道理的,因为我们已经确保剩余的数不能有小的素数。实际上,我们的候选清单中的第三个(2534536543597)是除N之外的第一个素数。我描述的筛分方案将倾向于得出质数或由通常较大的质数组成的数。因此,在找到下一个素数之前,我们实际上只需要对几个素数进行显式检验。
类似的方案很快产生超过N = 1000000000000000000000000000000000的下一个素数,即1000000000000000000000000103。
没有函数f(n)计算下一个素数。相反,必须测试一个数字的素性。
查找第n个质数时,也已经知道从第1个素数到第(n-1)个素数,这也是非常有用的,因为这些是唯一需要作为因子进行检验的数。
由于这些原因,如果预先计算出一组大质数,我不会感到惊讶。如果需要重复重新计算某些素数,对我来说真的没有意义。
正如其他人已经指出的那样,在给定当前素数的情况下,尚未找到找到下一个素数的方法。因此,由于必须检查已知素数和下一个素数之间的n / 2个数,因此大多数算法都将重点放在使用快速素数检查方法上。
如Paul Wheeler所述,根据应用程序的不同,您还可以仅对查找表进行硬编码。
对于纯粹的新颖性,总有这种方法:
#!/usr/bin/perl
for $p ( 2 .. 200 ) {
next if (1x$p) =~ /^(11+)\1+$/;
for ($n=1x(1+$p); $n =~ /^(11+)\1+$/; $n.=1) { }
printf "next prime after %d is %d\n", $p, length($n);
}
当然会产生
next prime after 2 is 3
next prime after 3 is 5
next prime after 5 is 7
next prime after 7 is 11
next prime after 11 is 13
next prime after 13 is 17
next prime after 17 is 19
next prime after 19 is 23
next prime after 23 is 29
next prime after 29 is 31
next prime after 31 is 37
next prime after 37 is 41
next prime after 41 is 43
next prime after 43 is 47
next prime after 47 is 53
next prime after 53 is 59
next prime after 59 is 61
next prime after 61 is 67
next prime after 67 is 71
next prime after 71 is 73
next prime after 73 is 79
next prime after 79 is 83
next prime after 83 is 89
next prime after 89 is 97
next prime after 97 is 101
next prime after 101 is 103
next prime after 103 is 107
next prime after 107 is 109
next prime after 109 is 113
next prime after 113 is 127
next prime after 127 is 131
next prime after 131 is 137
next prime after 137 is 139
next prime after 139 is 149
next prime after 149 is 151
next prime after 151 is 157
next prime after 157 is 163
next prime after 163 is 167
next prime after 167 is 173
next prime after 173 is 179
next prime after 179 is 181
next prime after 181 is 191
next prime after 191 is 193
next prime after 193 is 197
next prime after 197 is 199
next prime after 199 is 211
除了所有娱乐和游戏,众所周知,最佳哈希表大小严格证明是该格式的素数4N−1
。因此,仅找到下一个素数是不够的。您也必须做其他检查。
据我所记得,它在当前大小的两倍旁边使用质数。它不会计算素数-在该表中预载了一些最大数值的数字(不完全是大约10,000,000左右的数字)。达到该数字时,它会使用一些朴素的算法来获取下一个数字(例如curNum = curNum + 1),并通过以下方法使用某些方法对其进行验证:http : //en.wikipedia.org/wiki/Prime_number#Verifying_primality
next_prime = prime + 2
。你可能是对的,没有人能证明一旦你足够高,你就永远错了。所以去吧。:)