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

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


当前回答

我希望此功能与所有正64位有符号整数

Math.sqrt()使用double作为输入参数,因此对于大于2^53的整数,不会得到准确的结果。

其他回答

如果速度是一个问题,为什么不将最常用的一组输入及其值划分到一个查找表中,然后执行您针对特殊情况提出的任何优化魔术算法?

如果你做了一个二进制斩试图找到“正确”的平方根,你可以很容易地检测到你得到的值是否足够接近:

(n+1)^2 = n^2 + 2n + 1
(n-1)^2 = n^2 - 2n + 1

因此,在计算了n^2之后,选项如下:

n ^2=目标:已完成,返回truen^2+2n+1>target>n^2:你很接近,但并不完美:return falsen^2-2n+1<目标<n^2:同上目标<n^2-2n+1:低位n上的二进制斩波目标>n^2+2n+1:较高n上的二进制斩波

(抱歉,这使用n作为您当前的猜测,并将其作为参数的目标。对此感到困惑深表歉意!)

我不知道这是否会更快,但值得一试。

编辑:二进制斩不必接受整个整数范围,或者(2^x)^2=2^(2x),所以一旦你在目标中找到了最高位(这可以用一个小技巧来完成;我完全忘记了怎么做),你就可以快速得到一系列可能的答案。请注意,一个简单的二进制斩仍然只需要31或32次迭代。

考虑到一般的比特长度(尽管我在这里使用了特定的类型),我试图设计如下的简单算法。最初需要对0,1,2或<0进行简单而明显的检查。以下是简单的,因为它不试图使用任何现有的数学函数。大多数运算符可以用逐位运算符替换。我还没有用任何基准数据进行测试。我既不是数学专家,也不是计算机算法设计专家,我很乐意看到你们指出这个问题。我知道那里有很多改进的机会。

int main()
{
    unsigned int c1=0 ,c2 = 0;  
    unsigned int x = 0;  
    unsigned int p = 0;  
    int k1 = 0;  
    scanf("%d",&p);  
    if(p % 2 == 0) {  
        x = p/2; 
    }  
    else {  
        x = (p/2) +1;  
    }  
    while(x) 
    {
        if((x*x) > p) {  
            c1 = x;  
            x = x/2; 
        }else {  
            c2 = x;  
            break;  
        }  
    }  
    if((p%2) != 0)  
        c2++;

    while(c2 < c1) 
    {  
        if((c2 * c2 ) == p) {  
            k1 = 1;  
            break;  
        }  
        c2++; 
    }  
    if(k1)  
        printf("\n Perfect square for %d", c2);  
    else  
        printf("\n Not perfect but nearest to :%d :", c2);  
    return 0;  
}  

用牛顿法计算平方根的速度快得惊人。。。只要起始值是合理的。然而,没有合理的起始值,在实践中,我们以平分和对数(2^64)行为结束。要真正做到快速,我们需要一种快速的方法来获得一个合理的初始值,这意味着我们需要进入机器语言。如果一个处理器在奔腾中提供了一个像POPCNT这样的指令,它对前导零进行计数,我们可以使用它来获得一个具有一半有效位的起始值。小心地,我们可以找到一个固定数量的牛顿步数,这将总是足够的。(因此,前面提到了需要循环并具有非常快的执行。)

第二种解决方案是通过浮点设备,它可能具有快速的sqrt计算(如i87协处理器)。即使通过exp()和log()进行偏移,也可能比牛顿退化为二进制搜索更快。这有一个棘手的方面,即依赖于处理器的分析,以确定后续是否需要改进。

第三种解决方案解决了一个稍有不同的问题,但很值得一提,因为问题中描述了情况。如果你想为稍有不同的数字计算很多平方根,你可以使用牛顿迭代,如果你从来没有重新初始化起始值,但只需将其保留在之前的计算停止的地方。我已经在至少一个欧拉问题中成功地使用了这一方法。

这是最简单和最简洁的方法,尽管我不知道它在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。