我正在寻找确定长值是否为完美平方(即其平方根是另一个整数)的最快方法:

我使用内置的Math.sqrt()以简单的方式完成了这项工作函数,但我想知道是否有一种方法可以通过将自己限制为仅限整数的域。维护查找表是不切实际的(因为平方小于263的231.5个整数)。

下面是我现在做的非常简单明了的方法:

public final static boolean isPerfectSquare(long n)
{
  if (n < 0)
    return false;

  long tst = (long)(Math.sqrt(n) + 0.5);
  return tst*tst == n;
}

注意:我在许多Project Euler问题中都使用了这个函数。因此,其他人将永远不必维护此代码。而这种微优化实际上可能会有所不同,因为挑战的一部分是在不到一分钟的时间内完成每一个算法,而在某些问题中,这个函数需要调用数百万次。


我尝试了不同的解决方案:

经过详尽的测试,我发现不需要在Math.sqrt()的结果上加0.5,至少在我的机器上是这样。快速平方根逆运算速度更快,但对于n>=410881,它给出了错误的结果。然而,正如BobbyShaftoe所建议的,我们可以在n<410881时使用FISR黑客。牛顿的方法比Math.sqrt()慢得多。这可能是因为Math.sqr()使用了类似于牛顿方法的东西,但在硬件中实现,所以比Java快得多。此外,牛顿法仍然需要使用双精度。一个经过修改的牛顿方法使用了一些技巧,因此只涉及整数数学,需要一些技巧来避免溢出(我希望这个函数可以处理所有64位有符号的正整数),而且它仍然比math.sqrt()慢。二元斩更慢。这是有意义的,因为二进制斩波平均需要16次才能找到64位数字的平方根。根据John的测试,在C++中使用or语句比使用switch更快,但在Java和C#中,or和switch之间似乎没有区别。我还尝试创建一个查找表(作为64个布尔值的私有静态数组)。然后,我只说if(lookup[(int)(n&0x3F)]){test}else return false;,而不是switch或or语句;。令我惊讶的是,这(只是稍微)慢了一些。这是因为在Java中检查数组边界。


当前回答

当观察到正方形的最后n位时,我检查了所有可能的结果。通过连续检查更多位,可以消除多达5/6的输入。我实际上是为了实现费马的因子分解算法而设计的,而且速度非常快。

public static boolean isSquare(final long val) {
   if ((val & 2) == 2 || (val & 7) == 5) {
     return false;
   }
   if ((val & 11) == 8 || (val & 31) == 20) {
     return false;
   }

   if ((val & 47) == 32 || (val & 127) == 80) {
     return false;
   }

   if ((val & 191) == 128 || (val & 511) == 320) {
     return false;
   }

   // if((val & a == b) || (val & c == d){
   //   return false;
   // }

   if (!modSq[(int) (val % modSq.length)]) {
        return false;
   }

   final long root = (long) Math.sqrt(val);
   return root * root == val;
}

伪代码的最后一位可用于扩展测试以消除更多值。上述测试针对k=0、1、2、3

a的形式为(3<<2k)-1b的形式为(2<<2k)c的形式为(2<<2k+2)-1d的形式为(2<<2k-1)*10

它首先测试它是否具有幂模为2的平方残差,然后根据最终模量进行测试,然后使用Math.sqrt进行最终测试。我从最上面的帖子中提出了这个想法,并试图扩展它。我感谢任何评论或建议。

更新:使用模数(modSq)和44352的模数基数的测试,我的测试在OP更新中的96%的时间内运行,最多可达1000000000。

其他回答

我不确定它是否会更快,甚至更准确,但你可以使用约翰·卡马克的神奇平方根算法来更快地解平方根。您可能很容易对所有可能的32位整数进行测试,并验证您实际上得到了正确的结果,因为这只是一个近似值。然而,现在我想起来,使用双打也是近似的,所以我不确定这会如何发挥作用。

关于卡马克方法,似乎只需要重复一次就很容易了,这应该会使精度位数翻倍。毕竟,这是一种极其简化的迭代方法——牛顿迭代法,具有很好的第一个猜测。

关于您当前的最佳状态,我看到了两个微观优化:

使用mod255在检查后移动检查而不是0重新排列4的除法幂,以跳过通常(75%)情况下的所有检查。

I.e:

// Divide out powers of 4 using binary search

if((n & 0x3L) == 0) {
  n >>=2;

  if((n & 0xffffffffL) == 0)
    n >>= 32;
  if((n & 0xffffL) == 0)
      n >>= 16;
  if((n & 0xffL) == 0)
      n >>= 8;
  if((n & 0xfL) == 0)
      n >>= 4;
  if((n & 0x3L) == 0)
      n >>= 2;
}

更好的方法可能是

while ((n & 0x03L) == 0) n >>= 2;

显然,了解每个检查点有多少数字被剔除是很有意思的——我更怀疑这些检查是否真正独立,这使得事情变得棘手。

我在想我在数值分析课程中度过的可怕时光。

然后我记得,在Quake源代码中,有一个函数围绕着“网络”旋转:

float Q_rsqrt( float number )
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;  // evil floating point bit level hacking
  i  = 0x5f3759df - ( i >> 1 ); // wtf?
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
  // y  = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) ); // bk010122 - FPE?
  #endif
  #endif
  return y;
}

它基本上使用牛顿近似函数(记不清确切的名字)计算平方根。

它应该是可用的,甚至可能更快,它来自一个非凡的id软件的游戏!

它是用C++编写的,但一旦你有了这样的想法,在Java中重用同样的技术应该不会太难:

我最初在以下位置找到它:http://www.codemaestro.com/reviews/9

牛顿的方法在维基百科上解释:http://en.wikipedia.org/wiki/Newton%27s_method

您可以通过链接了解更多的工作原理,但如果您不太在意,那么这大概是我在阅读博客和参加数值分析课程时所记得的:

*(long*)&y基本上是一个快速转换为long的函数,因此整数运算可以应用于原始字节。0x5f3759df-(i>>1);line是近似函数的预先计算的种子值。*(float*)-i将值转换回浮点。y=y*(three-half-(x2*y*y))行基本上再次迭代函数上的值。

在结果上迭代函数的次数越多,逼近函数给出的值就越精确。在Quake的案例中,一次迭代“足够好”,但如果不是为了你。。。然后您可以添加所需的迭代次数。

这应该更快,因为它减少了在简单平方根中执行的除法运算的数量(实际上是一个*0.5F乘法运算),并用一些固定数量的乘法运算代替。

一个数字的平方根,假设该数字是一个完全平方。

复杂性为log(n)

/**
 * Calculate square root if the given number is a perfect square.
 * 
 * Approach: Sum of n odd numbers is equals to the square root of n*n, given 
 * that n is a perfect square.
 *
 * @param number
 * @return squareRoot
 */

public static int calculateSquareRoot(int number) {

    int sum=1;
    int count =1;
    int squareRoot=1;
    while(sum<number) {
        count+=2;
        sum+=count;
        squareRoot++;
    }
    return squareRoot;
}

不确定这是否是最快的方法,但这是我(很久以前在高中)在数学课上无聊地玩计算器时偶然发现的。当时,我真的很惊讶这是有效的。。。

public static boolean isIntRoot(int number) {
    return isIntRootHelper(number, 1);
}

private static boolean isIntRootHelper(int number, int index) {
    if (number == index) {
        return true;
    }
    if (number < index) {
        return false;
    }
    else {
        return isIntRootHelper(number - 2 * index, index + 1);
    }
}