我正在寻找确定long
值是否是完美平方(即,其平方根是另一个整数)的最快方法:
- 通过使用内置的
Math.sqrt()
函数,我已经完成了简单的方法,但是我想知道是否有一种方法可以通过将自己限制在仅整数域中来更快地完成操作。 - 维护查找表是不切实际的(因为大约有2 31.5个整数,其平方小于2 63 )。
这是我现在做的非常简单明了的方法:
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 hack。
- 牛顿的方法比
Math.sqrt()
慢很多。 这可能是因为Math.sqrt()
使用类似于牛顿方法的方法,但是在硬件中实现,因此它比Java快得多。 同样,牛顿法仍然需要使用双精度。 - 一种经过改进的牛顿方法,其中使用了一些技巧,以便仅涉及整数数学,因此需要一些技巧来避免溢出(我希望此函数与所有正的64位带符号整数一起使用),但它仍然比
Math.sqrt()
慢Math.sqrt()
。 - 二进制印章甚至更慢。 这是有道理的,因为二进制印章平均需要16次通过才能找到64位数字的平方根。
- 根据John的测试,在C ++中使用
or
语句比使用switch
更快,但是在Java和C#中,or
和switch
之间似乎没有区别。 - 我还尝试制作一个查找表(作为64个布尔值的私有静态数组)。 然后,我会说
if(lookup[(int)(n&0x3F)]) { test } else return false;
而不是switch或or
语句,if(lookup[(int)(n&0x3F)]) { test } else return false;
。 令我惊讶的是,这慢了一点。 这是因为数组边界是在Java中检查的 。
#1楼
“我正在寻找确定长值是否是完美平方(即,其平方根是另一个整数)的最快方法。”
答案令人印象深刻,但我看不到一个简单的检查:
检查长整数右边的第一个数字是否是集合(0,1,4,5,6,9)的成员。 如果不是,则不可能是“完美的正方形”。
例如。
4567-不可能是完美的正方形。
#2楼
我对该线程中的几种算法进行了自己的分析,并得出了一些新结果。 您可以在此答案的编辑历史记录中看到那些较旧的结果,但是由于我犯了一个错误,它们并不准确,并且浪费了时间来分析几种不太接近的算法。 但是,从几个不同的答案中吸取教训,我现在有两种算法可以压碎该线程的“赢家”。 这是我与其他人不同的核心工作:
// This is faster because a number is divisible by 2^4 or more only 6% of the time
// and more than that a vanishingly small percentage.
while((x & 0x3) == 0) x >>= 2;
// This is effectively the same as the switch-case statement used in the original
// answer.
if((x & 0x7) != 1) return false;
但是,这种简单的行(通常在大多数情况下添加一条或两条非常快的指令)大大简化了将switch-case
语句简化为一个if语句的过程。 但是,如果许多测试数字具有显着的2的幂,它可能会增加运行时间。
以下算法如下:
- 互联网 -Kip发布的答案
- 杜伦(Durron) -我的修改后的答案以单次通过答案为基础
- DurronTwo-我的修改后的答案使用了两次通过答案(@JohnnyHeggheim),并进行了其他一些小的修改。
如果数字是使用Math.abs(java.util.Random.nextLong())
生成的,这是一个示例运行时
0% Scenario{vm=java, trial=0, benchmark=Internet} 39673.40 ns; ?=378.78 ns @ 3 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 37785.75 ns; ?=478.86 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronTwo} 35978.10 ns; ?=734.10 ns @ 10 trials
benchmark us linear runtime
Internet 39.7 ==============================
Durron 37.8 ============================
DurronTwo 36.0 ===========================
vm: java
trial: 0
如果仅在前一百万个long上运行,则这是一个示例运行时:
0% Scenario{vm=java, trial=0, benchmark=Internet} 2933380.84 ns; ?=56939.84 ns @ 10 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 2243266.81 ns; ?=50537.62 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronTwo} 3159227.68 ns; ?=10766.22 ns @ 3 trials
benchmark ms linear runtime
Internet 2.93 ===========================
Durron 2.24 =====================
DurronTwo 3.16 ==============================
vm: java
trial: 0
如您所见, DurronTwo
在大型输入上表现更好,因为它非常频繁地使用魔术,但是与第一种算法和Math.sqrt
相比,它变得DurronTwo
,因为数字小得多。 同时,较简单的Durron
是一个巨大的赢家,因为在前100万个数字中,它Durron
将其除以4。
这是Durron
:
public final static boolean isPerfectSquareDurron(long n) {
if(n < 0) return false;
if(n == 0) return true;
long x = n;
// This is faster because a number is divisible by 16 only 6% of the time
// and more than that a vanishingly small percentage.
while((x & 0x3) == 0) x >>= 2;
// This is effectively the same as the switch-case statement used in the original
// answer.
if((x & 0x7) == 1) {
long sqrt;
if(x < 410881L)
{
int i;
float x2, y;
x2 = x * 0.5F;
y = x;
i = Float.floatToRawIntBits(y);
i = 0x5f3759df - ( i >> 1 );
y = Float.intBitsToFloat(i);
y = y * ( 1.5F - ( x2 * y * y ) );
sqrt = (long)(1.0F/y);
} else {
sqrt = (long) Math.sqrt(x);
}
return sqrt*sqrt == x;
}
return false;
}
和DurronTwo
public final static boolean isPerfectSquareDurronTwo(long n) {
if(n < 0) return false;
// Needed to prevent infinite loop
if(n == 0) return true;
long x = n;
while((x & 0x3) == 0) x >>= 2;
if((x & 0x7) == 1) {
long sqrt;
if (x < 41529141369L) {
int i;
float x2, y;
x2 = x * 0.5F;
y = x;
i = Float.floatToRawIntBits(y);
//using the magic number from
//http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
//since it more accurate
i = 0x5f375a86 - (i >> 1);
y = Float.intBitsToFloat(i);
y = y * (1.5F - (x2 * y * y));
y = y * (1.5F - (x2 * y * y)); //Newton iteration, more accurate
sqrt = (long) ((1.0F/y) + 0.2);
} else {
//Carmack hack gives incorrect answer for n >= 41529141369.
sqrt = (long) Math.sqrt(x);
}
return sqrt*sqrt == x;
}
return false;
}
我的基准测试工具:(需要Google caliper 0.1-rc5)
public class SquareRootBenchmark {
public static class Benchmark1 extends SimpleBenchmark {
private static final int ARRAY_SIZE = 10000;
long[] trials = new long[ARRAY_SIZE];
@Override
protected void setUp() throws Exception {
Random r = new Random();
for (int i = 0; i < ARRAY_SIZE; i++) {
trials[i] = Math.abs(r.nextLong());
}
}
public int timeInternet(int reps) {
int trues = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < ARRAY_SIZE; j++) {
if(SquareRootAlgs.isPerfectSquareInternet(trials[j])) trues++;
}
}
return trues;
}
public int timeDurron(int reps) {
int trues = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < ARRAY_SIZE; j++) {
if(SquareRootAlgs.isPerfectSquareDurron(trials[j])) trues++;
}
}
return trues;
}
public int timeDurronTwo(int reps) {
int trues = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < ARRAY_SIZE; j++) {
if(SquareRootAlgs.isPerfectSquareDurronTwo(trials[j])) trues++;
}
}
return trues;
}
}
public static void main(String... args) {
Runner.main(Benchmark1.class, args);
}
}
更新:我做了一个新的算法,在某些情况下更快,而在另一些情况下则很慢,我根据不同的输入获得了不同的基准。 如果我们计算模0xFFFFFF = 3 x 3 x 5 x 7 x 13 x 17 x 241
,则可以消除97.82%不能为平方的数字。 可以用5个按位运算在一行中完成(某种程度上):
if (!goodLookupSquares[(int) ((n & 0xFFFFFFl) + ((n >> 24) & 0xFFFFFFl) + (n >> 48))]) return false;
所得索引为1)残基,2)残基+ 0xFFFFFF
,或3)残基+ 0x1FFFFFE
。 当然,我们需要有一个残差查找表,其模为0xFFFFFF
,大约为3mb文件(在这种情况下,存储为ascii文本十进制数字,不是最佳值,但显然可以通过ByteBuffer
改进,依此类推。)但这是预先计算的结果没关系, 您可以在这里找到文件 (或自己生成文件 ):
public final static boolean isPerfectSq