【问题标题】:Fastest way to determine if an integer's square root is an integer确定整数平方根是否为整数的最快方法
【发布时间】:2010-09-22 16:11:01
【问题描述】:

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

  1. 我使用内置的Math.sqrt() 以简单的方式做到了这一点 功能,但我想知道是否有办法更快地做到这一点 将自己限制在仅整数域。
  2. 维护查找表是不切实际的(因为大约有 231.5 平方小于 263 的整数。

这是我现在正在做的非常简单直接的方法:

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 是没有必要的,至少在我的机器上没有。
  • fast inverse square root 更快,但它在 n >= 410881 时给出了不正确的结果。但是,正如 BobbyShaftoe 所建议的,我们可以对 n
  • Newton 的方法比Math.sqrt() 慢了很多。这可能是因为Math.sqrt() 使用类似于牛顿法的东西,但在硬件中实现,因此它比在 Java 中快得多。此外,牛顿法仍然需要使用双精度数。
  • 修改后的牛顿方法,它使用了一些技巧,因此只涉及整数数学,需要一些技巧来避免溢出(我希望这个函数可以处理所有正的 64 位有符号整数),它仍然比Math.sqrt()
  • 二进制斩波甚至更慢。这是有道理的,因为二进制斩波平均需要 16 次通过才能找到 64 位数字的平方根。
  • 根据 John 的测试,在 C++ 中使用 or 语句比使用 switch 更快,但在 Java 和 C# 中,orswitch 之间似乎没有区别。
  • 我还尝试制作一个查找表(作为 64 个布尔值的私有静态数组)。然后不是 switch 或or 语句,我只想说if(lookup[(int)(n&amp;0x3F)]) { test } else return false;。令我惊讶的是,这(只是稍微)慢了一点。这是因为array bounds are checked in Java

【问题讨论】:

  • 这是 Java 代码,其中 int==32 位和 long==64 位,并且都是有符号的。
  • @Shreevasta:我已经对大值(大于 2^53)进行了一些测试,您的方法给出了一些误报。遇到的第一个是 n=9007199326062755,它不是一个完美的正方形,而是作为一个返回。
  • 请不要将其称为“John Carmack hack”。不是他想出来的。
  • @mamama -- 或许,但这要归功于他。亨利福特没有发明汽车,莱特兄弟没有发明飞机,加莱奥也不是第一个发现地球绕太阳转的人……世界是由偷来的发明组成的(而且爱)。
  • 您可能会通过使用((1&lt;&lt;(n&amp;15))|65004) != 0 之类的方法来略微提高“快速失败”的速度,而不是进行三个单独的检查。

标签: java math optimization perfect-square


【解决方案1】:

这是最简单和最简洁的方法,虽然我不知道它在 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 - 10,000,000 次上运行此算法,每次计算平均需要 1000 - 1100 纳秒。

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

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

【讨论】:

  • 我想你的意思是(long) Math.pow(roundedRoot, 2)
  • 我更新了这个解决方案/建议,使其成为解决问题的最简单方法。我想知道它与一些自定义解决方案的基准比较。
  • 你的方法为10_000_000_000_000_000L10_000_000_000_000_001L返回true,这证明它没有错。
  • @aventurin 我已经更改了答案,以应对您对我之前尝试的准确评估。如果您对我的评论投了反对票,请重新考虑。谢谢!
【解决方案2】:

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

【讨论】:

  • long 范围内有 2^32 个完美正方形。这张桌子会很大。此外,计算值相对于内存访问的优势可能是巨大的。
  • 哦不,没有,有 2^16。 2^32 是 2^16 的平方。有 2^16。
  • 是的,但是 long 的范围是 64 位,而不是 32 位。平方(2^64)=2^32。 (我忽略了符号位以使数学更容易......实际上有 (long)(2^31.5)=3037000499 完美的正方形)
【解决方案3】:

关于 Carmac 方法,再次迭代似乎很容易,这应该会使准确度的位数增加一倍。毕竟,它是一种非常截断的迭代方法——牛顿法,第一次猜测非常好。

关于你目前的最佳状态,我看到了两个微优化:

  • 使用 mod255 在检查后将检查与 0 移动
  • 重新排列四的除法以跳过通常 (75%) 情况的所有检查。

即:

// 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;

显然,知道每个检查点有多少数字被剔除会很有趣——我相当怀疑这些检查是否真正独立,这让事情变得很棘手。

【讨论】:

    【解决方案4】:

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

    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);
        }
    }
    

    【讨论】:

    • 哎呀,这是一个 O(N^.5) 算法,所以它对速度真的很糟糕,并且对于可能输入的 63 位数字永远持续下去。我将赞成票更改为反对票。当我投票时我在想什么。至少它背后的想法是正确的,但我没有测试它。
    【解决方案5】:

    不知道最快,但最简单的方法是按正常方式取平方根,然后将结果乘以自己,看看它是否与您的原始值匹配。

    由于我们在这里讨论整数,所以禁食可能会涉及一个集合,您可以在其中进行查找。

    【讨论】:

    • “以正常方式取平方根”并检查它是否为 int 不是更快更便宜吗?
    • 我想这取决于你的系统上是 mod op 还是 mult op 更快。
    • 检查我上面的建议。它采用一种非常简单的方法。但是您也可以使用 sqrt 函数并将其修改为 1。我将添加它作为另一个建议的解决方案。
    猜你喜欢
    • 2014-04-09
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 2016-03-15
    • 1970-01-01
    • 1970-01-01
    • 2015-09-16
    相关资源
    最近更新 更多