项目Euler 35:HashSet给出不正确的结果

时间:2015-06-26 22:31:25

标签: java algorithm performance

我为Project Euler #35: Circular Primes编写了一个Java程序:

  

这个数字,197,被称为圆形素数,因为所有的旋转   数字:197,971和719本身就是素数。

     

在100:2,3,5,7,11,13,17,31之下有十三个这样的素数,   37,71,73,79和97。

     

在一百万以下有多少个圆形素数?

我的代码编译并运行正常,但是,根据我使用的数据结构,它会给出不同的结果。

算法的工作原理如下:

  1. 获取预先计算的素数。这是对MathUtils.getPrimes(1000000)的调用,它使所有素数等于或小于一百万。我将它存储在另一个Set中,因为它是通过返回一个子集来实​​现的,除非我将素数复制到他们自己的数据结构中,否则性能是非常可怕的。

  2. 虽然素数集不是空的,但是获得下一个素数。

  3. 获得该素数的所有轮换。例如。 197,971,719。这些轮换本身并不是主要的,因为无论如何我都需要验证它们。

  4. 如果素数集包含所有旋转,请将旋转计数添加到运行总计中。

  5. 如果存在,则从素数集中移除所有旋转。

  6. 我注意到这个代码有两个奇怪的地方。如果我使用TreeSet来存储素数,则性能非常快并且会产生正确的结果:

      

    答案:55
      时间:76ms

    如果我切换到HashSet,性能会差很多并且结果不正确

      

    答案:50
      时间:2527毫秒

    我把代码放在顶部,以便在代码运行之前仔细检查这两个集合是否包含相同的值,并且它们总是这样做。

    1. HashSet相比,为什么使用TreeSet会产生错误的结果?没有空值或其他奇怪的值,只有正的,不同的Integer个实例。这些集合开始包含完全相同的数据。算法是相同的,因为它是完全相同的代码。由于实现与数据大小之间的排序差异,几乎不可能比较算法运行时的状态。如果我减小输入大小,两者会产生相同的结果,最高可达100,000。

    2. 为什么TreeSet的执行速度比HashSet快得多,因为它必须执行所有不适用于HashSet的删除和树轮转?查看支持HashMap的{​​{1}}的代码,除了本地化到特定bin之外,不会调整内容的大小或改组。此外,素数相当均匀。虽然没有简单的验证方法,但我认为表中不会有很多项目占用最少数量的垃圾箱。

    3. 代码如下。您可以通过在顶部交换变量名来切换HashSet实现。

      Set

2 个答案:

答案 0 :(得分:2)

您的代码中有2个错误:

1)订单很重要。示例:2是通过旋转测试的素数。 20不是。旋转20是2.因此,如果首先随机迭代超过20,则代码将删除2而不计算它。这是对getRotations函数的更改,它将导致Tree / Hash Set具有相同的结果:

int current = start;
do {
   int currMagnitude = 1;
   for (int i = current; i > 9; i /= 10) {
      currMagnitude *= 10;
   }
   if (currMagnitude == magnitude)
       results.add(current);
   current = ((current % 10) * magnitude) + (current / 10);
} while (current != start);

2)当你迭代它时,你正在删除集合中的元素。你不应该用Java来做这件事。我怀疑如果您修改了这样的代码,TreeSet和HashSet的速度大致相同:

Collection<Integer> primesCopy = new HashSet<>(primes);
for(Integer i in primesCopy) {
     if(!primes.contains(i)) continue;
     // rest of code as it was

答案 1 :(得分:1)

一些摆弄表明,使用hashset的最昂贵的位是通过Integer next = primes.iterator().next();找到下一个要检查的素数 - 在我的机器上,使用hashset的版本大约需要4秒,其花费大约为3.9与迭代器相关的业务的秒数。

HashSet基于HashMap,其迭代器必须遍历所有桶,直到找到非空桶为止;据我所知,从HashMap的源代码中可以看出,删除后它不会自动调整大小,即一旦你将它带到一定的容量,如果不这样做,你将不得不手动调整大小。插入其中。这可能会导致一旦删除了HashSet的大部分元素,其大多数桶都是空的,因此找到第一个非空桶变得昂贵。关于为什么从HashSet中删除不会触发调整大小的最佳猜测是它没有构建节省空间和快速迭代。

树集不会发生这种情况;它仍然很浅(log 2 128000大约是17,所以它的最大深度是因为75k到80k的质数在10 ^ 6以下),它需要做的就是逐步进入它最左边的元素找到下一个。

这并没有解释我的机器的整个事件,因为即使忽略这一点,hashset比treeset贵了大约30%。我最好的猜测为什么会发生这种情况是散列整数是额外的负载,比在树集中查找整数键更昂贵,但这几乎不是猜测,当然不是一个坚实的论据。