这是一个理论上比O(n log(n)) 更好但合理的n 可能更差的算法。我相信它的运行时间是O(n lg*(n)),其中lg* 是https://en.wikipedia.org/wiki/Iterated_logarithm。
首先,您可以使用阿特金筛在时间O(n) 中找到直到n 的所有素数。详情请见https://en.wikipedia.org/wiki/Sieve_of_Atkin。
现在的想法是,我们将建立计数列表,每个计数只插入一次。我们将一个一个地检查素数,并为所有以它作为最大素数的东西插入值。然而,为了做到这一点,我们需要一个具有以下属性的数据结构:
- 我们可以为每个值存储一个值(特别是计数)。
- 我们可以在
O(1) 中前后遍历插入值列表。
- 我们可以在
i“高效”下方找到最后插入的数字。
- 插入应该是“高效的”。
(报价是难以估计的部分。)
第一个是微不足道的,我们数据结构中的每个槽都需要一个位置来存储值。第二个可以用双向链表来完成。第三个可以通过对跳过列表的巧妙变化来完成。第四个从前三个中掉出来。
我们可以用一个节点数组(一开始没有初始化)来做到这一点,其中包含以下字段,看起来像一个双向链表:
-
value我们正在寻找的答案。
-
prev 上一个我们有答案的值。
-
next 下一个我们有答案的值。
现在如果i 在列表中并且j 是下一个值,跳过列表的技巧将是我们还将在i 之后填写prev 作为第一个,第一个可以被除以4,可被 8 整除,以此类推,直到达到 j。因此,如果i = 81 和j = 96 我们将填写prev 为82, 84, 88 然后96。
现在假设我们要在现有的i 和j 之间的k 处插入一个值v。我们该怎么做呢?我将展示仅以 k known 开头的伪代码,然后填写 i = 81、j = 96 和 k = 90。
k.value := v
for temp in searching down from k for increasing factors of 2:
if temp has a value:
our_prev := temp
break
else if temp has a prev:
our_prev = temp.prev
break
our_next := our_prev.next
our_prev.next := k
k.next := our_next
our_next.prev := k
for temp in searching up from k for increasing factors of 2:
if j <= temp:
break
temp.prev = k
k.prev := our_prev
在我们的特定示例中,我们愿意从90 向下搜索到90, 88, 80, 64, 0。但是当我们到达88 时,我们实际上被告知prev 是81。我们愿意搜索到90, 92, 96, 128, 256, ...,但是我们只需要设置92.prev 96.prev 就可以了。
现在这是一段复杂的代码,但它的性能是O(log(k-i) + log(j-k) + 1)。这意味着它以 O(log(n)) 开始,但随着更多值的填写而变得更好。
那么我们如何初始化这个数据结构呢?好吧,我们初始化一个未初始化值的数组,然后设置1.value := 0、1.next := n+1 和2.prev := 4.prev := 8.prev := 16.prev := ... := 1。然后我们开始处理我们的素数。
当我们到达素数p 时,我们首先在n/p 下方搜索先前插入的值。从那里向后我们不断插入x*p, x*p^2, ... 的值,直到达到我们的极限。 (向后的原因是我们不想尝试在 3 中插入 18 一次,在 9 中插入一次。向后移动可以防止这种情况。)
现在我们的运行时间是多少?寻找素数是O(n)。找到初始插入也很容易O(n/log(n)) 时间操作O(log(n)) 另一个O(n)。现在所有值的插入呢?那是微不足道的O(n log(n)),但我们能做得更好吗?
首先所有插入到密度1/log(n) 填写的内容都可以及时完成O(n/log(n)) * O(log(n)) = O(n)。然后所有对密度1/log(log(n)) 的插入同样可以及时完成O(n/log(log(n))) * O(log(log(n))) = O(n)。随着日志数量的增加,依此类推。对于我给出的O(n lg*(n)) 估计值,我们得到的此类因子的数量是O(lg*(n))。
我没有证明这个估计值和你能做的一样好,但我认为它是。
所以,不是O(n),而是非常接近。