Java中判断整数平方根是否为整数的最快方法

Java中判断整数平方根是否为整数的最快方法

技术背景

在处理一些数学相关的问题时,经常需要判断一个整数的平方根是否为另一个整数,即该整数是否为完全平方数。例如在Project Euler等算法挑战中,此类判断可能需要被调用数百万次,因此算法的性能至关重要。在Java中,最直接的方法是使用Math.sqrt()函数,但该方法可能存在性能瓶颈,因此需要探索更高效的解决方案。

实现步骤

1. 简单直接法

1
2
3
4
5
6
7
public final static boolean isPerfectSquare(long n) {
if (n < 0)
return false;

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

该方法首先判断输入的数是否为负数,若是则直接返回false。然后使用Math.sqrt()函数计算平方根,并将结果四舍五入为长整型,最后判断该整数的平方是否等于原数。

2. 快速过滤法

1
2
3
4
5
6
7
8
9
10
11
12
public final static boolean isPerfectSquare(long n) {
if (n < 0)
return false;

switch((int)(n & 0xF)) {
case 0: case 1: case 4: case 9:
long tst = (long)Math.sqrt(n);
return tst*tst == n;
default:
return false;
}
}

此方法在调用Math.sqrt()之前,先通过位运算获取输入数的最后4位(十六进制的最后一位)。由于完全平方数在十六进制下的最后一位只能是0、1、4或9,因此对于不满足此条件的数可以直接返回false,从而减少不必要的平方根计算。

3. 综合优化法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
long goodMask; 
{
for (int i=0; i<64; ++i) goodMask |= Long.MIN_VALUE >>> (i*i);
}

public boolean isSquare(long x) {
if (goodMask << x >= 0) return false;
final int numberOfTrailingZeros = Long.numberOfTrailingZeros(x);
if ((numberOfTrailingZeros & 1) != 0) return false;
x >>= numberOfTrailingZeros;
if ((x&7) != 1 | x <= 0) return x == 0;
final long tst = (long) Math.sqrt(x);
return tst * tst == x;
}
  • 第一步,通过一个long类型的goodMask进行初步过滤,该掩码用于检查输入数的最后6位是否符合完全平方数的特征。
  • 第二步,使用Long.numberOfTrailingZeros()函数计算输入数末尾连续0的个数,若为奇数则直接返回false,因为完全平方数的质因数分解中,2的指数必须为偶数。
  • 第三步,移除末尾的0后,检查剩余数的二进制表示是否以001结尾,以及是否为正数。
  • 最后,使用Math.sqrt()函数进行最终判断。

4. 基于模运算和Hensel引理的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
typedef signed long long int int64;

int start[1024] = {...};
bool bad255[512] = {...};

inline bool square( int64 x ) {
if( x < 0 || (x&2) || ((x & 7) == 5) || ((x & 11) == 8) )
return false;
if( x == 0 )
return true;

int64 y = x;
y = (y & 4294967295LL) + (y >> 32);
y = (y & 65535) + (y >> 16);
y = (y & 255) + ((y >> 8) & 255) + (y >> 16);
if( bad255[y] )
return false;

if((x & 4294967295LL) == 0)
x >>= 32;
if((x & 65535) == 0)
x >>= 16;
if((x & 255) == 0)
x >>= 8;
if((x & 15) == 0)
x >>= 4;
if((x & 3) == 0)
x >>= 2;

if((x & 7) != 1)
return false;

int64 r, t, z;
r = start[(x >> 3) & 1023];
do {
z = x - r * r;
if( z == 0 )
return true;
if( z < 0 )
return false;
t = z & (-z);
r += (z & t) >> 1;
if( r > (t >> 1) )
r = t - r;
} while( t <= (1LL << 33) );

return false;
}
  • 首先进行快速过滤,排除负数和一些明显不是完全平方数的情况。
  • 然后计算输入数对255取模的结果,并通过bad255数组进行判断。
  • 接着使用二进制搜索去除输入数中2的幂次方。
  • 最后,使用类似于Hensel引理的方法计算平方根,并进行判断。

核心代码

以下是一个综合多种优化方法的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SquareRootChecker {
long goodMask;
{
for (int i=0; i<64; ++i) goodMask |= Long.MIN_VALUE >>> (i*i);
}

public boolean isSquare(long x) {
if (goodMask << x >= 0) return false;
final int numberOfTrailingZeros = Long.numberOfTrailingZeros(x);
if ((numberOfTrailingZeros & 1) != 0) return false;
x >>= numberOfTrailingZeros;
if ((x&7) != 1 | x <= 0) return x == 0;
final long tst = (long) Math.sqrt(x);
return tst * tst == x;
}
}

最佳实践

  • 提前过滤:在进行平方根计算之前,通过位运算、模运算等方法快速排除一些明显不是完全平方数的情况,减少不必要的计算。
  • 使用预计算表:如bad255数组和goodMask,可以在常数时间内进行判断,提高效率。
  • 结合多种方法:根据输入数据的特点,结合不同的优化方法,以达到最佳的性能。

常见问题

1. Math.sqrt()的精度问题

对于大于2^53的整数,Math.sqrt()返回的结果可能存在精度损失。但在判断完全平方数时,只要将结果转换为整数后进行平方比较,通常可以得到正确的结果。

2. 算法性能受输入数据分布的影响

不同的算法在不同的输入数据分布下可能表现不同。例如,某些算法在处理大量小整数时可能更高效,而另一些算法在处理大整数时可能更有优势。因此,在选择算法时,需要考虑输入数据的特点。

3. 数组边界检查的开销

在Java中,数组访问会进行边界检查,这可能会带来一定的性能开销。因此,在使用数组进行预计算表时,需要注意这一点。可以考虑使用位运算等方式避免数组访问,或者使用long类型的掩码来代替数组。


Java中判断整数平方根是否为整数的最快方法
https://119291.xyz/posts/2025-04-27.java-fastest-way-to-determine-integer-square-root/
作者
ww
发布于
2025年4月27日
许可协议