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

我使用内置的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中检查数组边界。


当前回答

为了记录在案,另一种方法是使用素分解。如果分解的每个因子都是偶数,那么这个数就是一个完美的平方。所以你想要的是看看一个数是否可以分解成质数平方的乘积。当然,你不需要获得这样的分解,只是为了看看它是否存在。

首先建立一个小于2^32的素数平方表。这远远小于一个包含所有整数的表,直到这个极限。

解决方案如下:

boolean isPerfectSquare(long number)
{
    if (number < 0) return false;
    if (number < 2) return true;

    for (int i = 0; ; i++)
    {
        long square = squareTable[i];
        if (square > number) return false;
        while (number % square == 0)
        {
            number /= square;
        }
        if (number == 1) return true;
    }
}

我想这有点神秘。它所做的是在每一步中检查质数的平方除以输入数。如果这样做了,那么它将尽可能地将数字除以平方,以从素数分解中删除这个平方。如果通过这个过程,我们得到1,那么输入数是素数平方的分解。如果平方比数字本身大,那么这个平方或任何更大的平方都无法分割它,所以数字不能是素数平方的分解。

考虑到现在的sqrt是在硬件中完成的,并且需要在这里计算素数,我想这个解决方案要慢得多。但正如mrzl在他的回答中所说,它应该比sqrt的解决方案给出更好的结果,sqrt的工作时间不会超过2^54。

其他回答

如果你想要速度,考虑到整数的大小是有限的,我想最快的方法是(a)按大小划分参数(例如,按最大位集划分类别),然后对照该范围内的完美平方数组检查值。

我参加聚会已经很晚了,但我希望能提供一个更好的答案;更短,(假设我的基准是正确的)也更快。

long goodMask; // 0xC840C04048404040 computed below
{
    for (int i=0; i<64; ++i) goodMask |= Long.MIN_VALUE >>> (i*i);
}

public boolean isSquare(long x) {
    // This tests if the 6 least significant bits are right.
    // Moving the to be tested bit to the highest position saves us masking.
    if (goodMask << x >= 0) return false;
    final int numberOfTrailingZeros = Long.numberOfTrailingZeros(x);
    // Each square ends with an even number of zeros.
    if ((numberOfTrailingZeros & 1) != 0) return false;
    x >>= numberOfTrailingZeros;
    // Now x is either 0 or odd.
    // In binary each odd square ends with 001.
    // Postpone the sign test until now; handle zero in the branch.
    if ((x&7) != 1 | x <= 0) return x == 0;
    // Do it in the classical way.
    // The correctness is not trivial as the conversion from long to double is lossy!
    final long tst = (long) Math.sqrt(x);
    return tst * tst == x;
}

第一个测试很快捕捉到大多数非正方形。它使用一个长的64项表,因此没有数组访问成本(间接和边界检查)。对于均匀随机的长,有81.25%的概率在这里结束。

第二个测试捕获因式分解中奇数为2的所有数字。Long.numberOfTrailingZeros方法非常快,因为它被JIT编译成一条i86指令。

删除尾随零后,第三个测试处理以二进制形式的011、101或111结尾的数字,这些数字不是完美的正方形。它还关心负数,也处理0。

最后的测试是双倍算术。由于double只有53位尾数,从long到double的转换包括大值的舍入。尽管如此,测试是正确的(除非证明是错误的)。

试图结合mod255的想法并不成功。

这是最简单和最简洁的方法,尽管我不知道它在CPU周期方面的比较。如果您只想知道根是否是整数,那么这非常有用。如果你真的关心它是不是整数,你也可以弄清楚。这里有一个简单(纯)函数:

private static final MathContext precision = new MathContext(20);

private static final Function<Long, Boolean> isRootWhole = (n) -> {
    long digit = n % 10;
    if (digit == 2 || digit == 3 || digit == 7 || digit == 8) {
        return false;
    }
    return new BigDecimal(n).sqrt(precision).scale() == 0;
};

如果您不需要微优化,那么这个答案在简单性和可维护性方面更好。如果要计算负数,则需要相应地处理,并将绝对值发送到函数中。我包含了一个小的优化,因为由于二次残差mod 10,没有完美的正方形具有2、3、7或8的十位数。

在我的CPU上,在0-10000000上运行此算法平均每次计算需要1000-1100纳秒。

如果执行的计算次数较少,则早期的计算需要更长的时间。

我有一个负面评论,说我以前的编辑不适用于大量数据。OP提到了Longs,Long的最大完美正方形是9223372030926249001,因此该方法适用于所有Longs。

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

整数牛顿法

如果希望避免非整数运算,可以使用以下方法。它基本上使用了为整数运算而修改的牛顿法。

/**
 * Test if the given number is a perfect square.
 * @param n Must be greater than 0 and less
 *    than Long.MAX_VALUE.
 * @return <code>true</code> if n is a perfect
 *    square, or <code>false</code> otherwise.
 */
public static boolean isSquare(long n)
{
    long x1 = n;
    long x2 = 1L;

    while (x1 > x2)
    {
        x1 = (x1 + x2) / 2L;
        x2 = n / x1;
    }

    return x1 == x2 && n % x1 == 0L;
}

此实现无法与使用Math.sqrt的解决方案竞争。但是,可以通过使用其他文章中描述的过滤机制来提高其性能。