返回

完全平方数判断:告别精度陷阱的多种方法

python

如何判断一个数是不是完全平方数?

咱们经常会遇到一个问题:给你一个整数 n,怎么判断它是不是一个「完全平方数」?所谓完全平方数,就是说它能不能表示成某个整数 x 的平方(x*x)。比如说,9 是个完全平方数,因为 3 * 3 = 9。但 10 就不是,找不到一个整数自乘等于 10。

这事儿听起来简单,但直接让计算机判断,会遇到一些小麻烦。

为啥不能直接开方看结果?

最直观的想法可能是:我直接对这个数 n 开平方,看看结果是不是整数不就行了?

比如用 Python 的 math.sqrt() 函数:

import math

n = 9
root = math.sqrt(n)
print(root) # 输出 3.0

n = 10
root = math.sqrt(n)
print(root) # 输出 3.1622776601683795

看起来,math.sqrt(9) 得到了 3.0,它的小数部分是 0,似乎说明 9 是完全平方数。而 math.sqrt(10) 的结果带有小数。

但是,这里面藏着坑!计算机处理浮点数(就是带小数的数)的时候,可能会有精度问题。有时候一个理论上应该是整数的开方结果,在计算机里可能存成类似 2.9999999999999996 或者 3.0000000000000004 这样的数。你直接判断它是不是 == 3.0 或者小数部分是不是 == 0,可能会出错。

另外,如果 n 是个超级大的数,开方运算本身的性能和精度也需要考虑。虽然题目说速度暂时不重要,但精度问题是绕不开的。

所以,我们需要更稳妥、更可靠的方法。

靠谱的判断方法

下面介绍几种常用且比较靠谱的判断思路。

方法一:循环枚举尝试

这是最朴素的想法:既然完全平方数是某个整数 x 的平方,那我就从小到大尝试呗。

原理和作用

我们从 i = 0 开始,依次计算 i*i 的值。

  • 如果 i*i 等于 n,那么 n 就是 i 的平方,妥妥的完全平方数。
  • 如果 i*i 超过了 n,那就没必要再往后试了。你想啊,后面 (i+1)*(i+1)(i+2)*(i+2)只会越来越大,肯定也超不过 n。这时候就可以断定 n 不是完全平方数。

代码示例 (Python)

def is_perfect_square_iter(n):
  """通过循环枚举判断n是否为完全平方数"""
  if n < 0: # 负数肯定不是
    return False
  if n == 0 or n == 1: # 0 和 1 是完全平方数
    return True

  i = 1
  while True:
    square = i * i
    if square == n:
      print(f"找到了!{i} * {i} == {n}")
      return True
    elif square > n:
      print(f"试到 {i} * {i} = {square},已经超过 {n} 了,没戏。")
      return False
    i += 1

# 测试
print(f"9 是完全平方数吗? {is_perfect_square_iter(9)}")
print(f"10 是完全平方数吗? {is_perfect_square_iter(10)}")
print(f"0 是完全平方数吗? {is_perfect_square_iter(0)}")
# print(f"-4 是完全平方数吗? {is_perfect_square_iter(-4)}") # 测试负数
# print(f"2147483647 是完全平方数吗? {is_perfect_square_iter(2147483647)}") # 测一个大一点的素数

安全建议与考量

  1. 处理负数和 0、1: 负数不可能是任何整数的平方(除非考虑复数,但我们通常讨论实数范围)。0 和 1 本身就是完全平方数(0*0=0, 1*1=1)。代码开头最好先处理这些特殊情况。
  2. 潜在的性能问题: 如果 n 非常大,比如 n = 10^18,那么 i 需要循环到 10^9 次,这会非常慢。对于性能要求高的场景,这方法不太行。
  3. 整数溢出风险(在某些语言中): 在像 C++ 或 Java 这样的语言里,如果 n 很大,计算 i * i 时,结果可能会超出标准整型(如 int)能表示的最大范围,导致溢出,结果就不对了。需要使用能容纳更大数值的类型(如 long long in C++, long in Java)。Python 的整数类型可以自动处理任意大的整数,没有这个问题。

进阶技巧

  • 循环的上限其实不需要到 n 那么大。i 的最大值只需要试到 sqrt(n) 附近就行。不过 while square <= n: 的判断条件已经隐式地处理了这一点。

方法二:利用数学库函数 + 整数验证

虽然前面说了直接用 math.sqrt() 有精度坑,但也不是完全不能用。关键在于用对方法。

原理和作用

  1. 计算 n 的浮点数平方根 root = math.sqrt(n)
  2. 如果 n 是完全平方数,root 理论上应该是整数。但由于精度问题,我们不能直接判断 root 是不是整数。
  3. 一个可靠的验证方法是:先把 root 强制转换成整数,得到 int_root = int(root)
  4. 然后,我们反过来计算 int_root * int_root,看看结果是否精确地等于原始的 n。如果相等,说明 n 确实是 int_root 这个整数的平方。

代码示例 (Python)

import math

def is_perfect_square_sqrt_check(n):
  """利用 math.sqrt 和整数回乘验证判断 n 是否为完全平方数"""
  if n < 0:
    return False
  if n == 0 or n == 1:
    return True

  # 1. 计算浮点数平方根
  root = math.sqrt(n)

  # 2. 取整
  int_root = int(root)

  # 3. 关键一步:用整数平方反向验证
  # 检查 int_root * int_root 是否精确等于 n
  is_perf_sq = (int_root * int_root == n)

  # (可选) 稍微更细致的检查:
  # 确保原始 root 和 int_root 足够接近 (虽然仅靠回乘检查通常已足够)
  # is_close_enough = math.isclose(root, int_root)
  # is_perf_sq = is_close_enough and (int_root * int_root == n)

  if is_perf_sq:
      print(f"sqrt({n}) ≈ {root}, 取整得到 {int_root}, 验证 {int_root}*{int_root} == {n} 成立。")
  else:
      # 注意:即使 n 不是完全平方数,int_root * int_root 也可能小于 n
      print(f"sqrt({n}) ≈ {root}, 取整得到 {int_root}, 验证 {int_root}*{int_root} ({int_root*int_root}) != {n}。")

  return is_perf_sq

# 测试
print(f"9 是完全平方数吗? {is_perfect_square_sqrt_check(9)}")
print(f"10 是完全平方数吗? {is_perfect_square_sqrt_check(10)}")
print(f"8 是完全平方数吗? {is_perfect_square_sqrt_check(8)}") # sqrt(8) ≈ 2.828, int_root = 2, 2*2=4 != 8
print(f"44100 是完全平方数吗? {is_perfect_square_sqrt_check(44100)}") # sqrt = 210.0

安全建议与考量

  1. 必须进行整数回乘验证: 千万不要只依赖 root == int(root) 或者 root % 1 == 0 这种判断,它们都可能因为浮点精度问题而出错。核心是 int(root) * int(root) == n
  2. 处理负数: math.sqrt() 函数不接受负数输入,会直接抛出 ValueError。所以必须在调用前检查 n 是否为负。
  3. 性能: math.sqrt() 通常是高度优化的,速度相当快。这种方法比暴力枚举快得多,尤其是对于大数。

进阶技巧:math.isqrt() (Python 3.8+)

从 Python 3.8 版本开始,math 模块提供了一个神器:math.isqrt(n)。这个函数直接计算 n整数平方根 (integer square root),也就是 floor(sqrt(n)),并且保证结果是整数。

这让判断过程变得超级简单直接:

import math

def is_perfect_square_isqrt(n):
  """使用 math.isqrt 判断 n 是否为完全平方数 (Python 3.8+)"""
  if n < 0:
    return False
  if not hasattr(math, 'isqrt'):
      print("警告:当前 Python 版本低于 3.8,不支持 math.isqrt。请使用其他方法。")
      # 在这里可以回退到 is_perfect_square_sqrt_check(n) 或其他方法
      return is_perfect_square_sqrt_check(n) # 示例回退

  # 计算整数平方根
  int_root = math.isqrt(n)

  # 验证整数平方根的平方是否等于原数
  is_perf_sq = (int_root * int_root == n)

  if is_perf_sq:
    print(f"isqrt({n}) = {int_root}, 验证 {int_root}*{int_root} == {n} 成立。")
  else:
    print(f"isqrt({n}) = {int_root}, 验证 {int_root}*{int_root} ({int_root*int_root}) != {n}。")

  return is_perf_sq

# 测试
# 确保你的 Python 版本 >= 3.8
print(f"9 是完全平方数吗? {is_perfect_square_isqrt(9)}")
print(f"10 是完全平方数吗? {is_perfect_square_isqrt(10)}")
print(f"25 是完全平方数吗? {is_perfect_square_isqrt(25)}")
print(f"12345 * 12345 = {12345 * 12345}")
large_square = 152399025
print(f"{large_square} 是完全平方数吗? {is_perfect_square_isqrt(large_square)}")
large_non_square = large_square + 1
print(f"{large_non_square} 是完全平方数吗? {is_perfect_square_isqrt(large_non_square)}")

math.isqrt() 的优点:

  • 简单明了: 代码逻辑最清晰。
  • 高效: 实现通常很优化。
  • 避免浮点数: 直接操作整数,彻底避开了浮点精度问题。
  • 推荐使用: 如果你的 Python 环境是 3.8 或更高版本,强烈推荐math.isqrt() 这个方法。

方法三:二分查找

如果不能用 math.sqrtmath.isqrt (比如某些受限环境或需要自己实现所有逻辑),或者想追求一种不依赖特定库函数的纯算法解法,二分查找是个不错的选择。

原理和作用

我们知道,如果 n 是一个完全平方数 x*x,那么这个 x 一定在 0 到 n 这个范围之间。(实际上,对于 n>1x 的范围更小,是在 0 到 n/2 + 1 之间,但用 0 到 n 也没问题,只是效率稍低)。

我们可以在 [0, n] (或更优化的范围) 这个区间内,用二分查找来寻找那个可能的整数 x

  1. 设置查找范围的下界 low = 0,上界 high = n
  2. low <= high 时,不断进行以下操作:
    • 计算中间值 mid = (low + high) // 2 (用 // 保证结果是整数)。
    • 计算 mid_square = mid * mid
    • 如果 mid_square == n,恭喜,找到了,n 是完全平方数,返回 True。
    • 如果 mid_square < n,说明 mid 太小了,我们需要找一个更大的 x。可能的 xmid 的右边,所以把查找区间的下界更新为 low = mid + 1
    • 如果 mid_square > n,说明 mid 太大了,我们需要找一个更小的 x。可能的 xmid 的左边,所以把查找区间的上界更新为 high = mid - 1
  3. 如果循环结束了(即 low > high),还没找到 mid*mid == n 的情况,说明在 0 到 n 的范围内找不到这样的整数 x,所以 n 不是完全平方数,返回 False。

代码示例 (Python)

def is_perfect_square_binary_search(n):
  """使用二分查找判断 n 是否为完全平方数"""
  if n < 0:
    return False
  if n == 0 or n == 1:
    return True

  low = 0
  high = n # 可以优化为 n // 2 + 1,但 n 就够了

  while low <= high:
    mid = (low + high) // 2
    # print(f"尝试: low={low}, high={high}, mid={mid}") # 调试时可以打开

    # 提前处理 mid == 0 的情况避免后续 0*0 判断可能混淆大数逻辑
    if mid == 0:
      # 如果 n 是 0, 之前已经返回 True。如果 n > 0, mid=0 的平方肯定小于 n。
      # 因此可以直接进入 mid*mid < n 的逻辑,即 low = mid + 1
      if n > 0:
        low = mid + 1
        continue
      else: # n 必须是 0,这种情况在函数开头已处理,逻辑上不会到这里
         pass

    # 为避免 mid*mid 溢出(虽然 Python 不会,但这是好习惯)
    # 可以用 n // mid 来比较 mid 与 sqrt(n) 的大小
    # if mid == n // mid and n % mid == 0: # 存在 mid*mid = n
    #  is_perf_sq = True
    # elif mid < n // mid: # mid < sqrt(n), 需要增大 mid
    #  low = mid + 1
    # else: # mid > sqrt(n), 需要减小 mid
    #  high = mid - 1
    # 下面是更直观的 mid * mid 判断 (Python 不会溢出)
    mid_square = mid * mid
    if mid_square == n:
      print(f"二分查找找到: mid={mid}, {mid}*{mid} == {n}")
      return True
    elif mid_square < n:
      low = mid + 1
    else: # mid_square > n
      high = mid - 1

  print(f"二分查找结束,未找到平方根。low={low}, high={high}")
  return False

# 测试
print(f"9 是完全平方数吗? {is_perfect_square_binary_search(9)}")
print(f"10 是完全平方数吗? {is_perfect_square_binary_search(10)}")
print(f"65536 是完全平方数吗? {is_perfect_square_binary_search(65536)}") # 256 * 256
large_square = 123456 * 123456
print(f"{large_square} 是完全平方数吗? {is_perfect_square_binary_search(large_square)}")
large_non_square = large_square + 1
print(f"{large_non_square} 是完全平方数吗? {is_perfect_square_binary_search(large_non_square)}")

安全建议与考量

  1. 整数溢出(非 Python 环境): 和方法一类似,计算 mid * mid 时,如果 n 很大,结果可能溢出。在 C++/Java 等语言中,要用 64 位整型(如 long long/long),并且可能需要在比较时做些技巧,比如用 mid > n / mid (注意处理 mid=0 的情况) 来替代 mid*mid > n,防止乘法溢出。
  2. 边界条件: low, high 的初始值和更新逻辑(mid+1, mid-1)以及循环条件(low <= high)要设置正确,避免死循环或错过答案。
  3. 性能: 二分查找的时间复杂度是 O(log n),对于非常大的 n,这比 O(sqrt(n)) 的暴力枚举快得多。它的性能通常与 math.sqrt / math.isqrt 相当或稍慢,但好处是完全不依赖浮点运算。

小结一下

判断一个数 n 是否是完全平方数,你有几种武器:

  1. 暴力枚举 (方法一): 简单直观,但对大数效率低。
  2. math.sqrt + 整数回乘验证 (方法二): 利用库函数,效率较高,但必须通过 int(root) * int(root) == n 来规避浮点精度问题。
  3. math.isqrt (方法二 进阶): Python 3.8+ 的最佳选择,简洁、高效、无精度陷阱。强烈推荐
  4. 二分查找 (方法三): 纯算法实现,不依赖浮点数,性能优秀 (O(log n)),是 math.isqrt 不可用时的好替代品,或面试中考察算法能力的常见题目。

根据你的具体环境(比如 Python 版本)和需求(比如是否允许使用库函数),可以选择最合适的方法。对于日常 Python 开发,优先考虑 math.isqrt 就对了。