news 2026/9/9 2:03:51

完全平方数判断:从二分查找防溢到牛顿迭代的算法精解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
完全平方数判断:从二分查找防溢到牛顿迭代的算法精解

在算法面试和日常刷题中,判断一个整数是否为完全平方数是一个经典且高频的问题。它看似简单,却巧妙地融合了二分查找、数学技巧和边界处理等多个基础知识点,是检验编程基本功和思维严谨性的绝佳题目。无论是准备校招、社招,还是参加华为OD机试、GESP认证,这类题目都频繁出现。

本文将围绕LeetCode 第367题「有效的完全平方数」,为你提供一份从暴力破解到最优解的完整攻略。我们将深入剖析每种解法的核心思想、代码实现、时间复杂度和易错点,并补充同类题目的解题思路(如“爱吃香蕉的狒狒”中涉及的二分查找思想)。无论你是算法新手,还是希望优化解法的进阶者,都能从中获得清晰的指引和可运行的代码示例。

1. 问题背景与核心概念

在开始解题之前,我们首先要明确“完全平方数”在编程问题中的定义和边界。

1.1 什么是完全平方数?

在数学上,完全平方数是指可以表示为某个整数的平方的数。例如:

  • 1 = 1²
  • 4 = 2²
  • 9 = 3²
  • 16 = 4²

在LeetCode第367题中,题目要求:给定一个非负整数num,编写一个函数来判断它是否是一个完全平方数。要求不能使用任何内置的库函数,如sqrt

输入输出示例:

  • 输入:num = 16

  • 输出:true

  • 解释:因为 4 * 4 = 16

  • 输入:num = 14

  • 输出:false

  • 解释:因为 3²=9 < 14 < 4²=16

1.2 为什么这道题重要?

  1. 考察基础算法:本题是练习二分查找的入门级经典应用,比在有序数组中查找目标值更进一层,需要你自行确定搜索范围。
  2. 考察边界与溢出处理:在计算中间值的平方时,使用int类型很可能导致溢出,这是本题主要的“坑点”之一。
  3. 连接多个知识点:除了二分法,还可以用牛顿迭代法(数学方法)来解决,这有助于理解不同算法范式对同一问题的思考角度。
  4. 高频出现:在“力扣热题100”、各类机试真题(如华为OD)和认证考试(如GESP)中,完全平方数及其变种问题都是常客。

1.3 问题边界与约束

  • 输入范围0 <= num <= 2³¹ - 1(即 2147483647)。这意味着我们必须考虑num为 0 和非常大的情况。
  • 禁止使用sqrt:这要求我们必须自己实现判断逻辑。
  • 返回值:布尔值truefalse

2. 环境准备与解题思路

我们不需要复杂的开发环境,任何支持你熟悉编程语言的IDE或在线编辑器均可。本文将主要使用JavaPython两种语言给出示例代码,因为它们分别是力扣平台上最主流和语法最简洁的语言之一。

核心解题思路演进:我们将按照从直观到高效、从易错到稳健的顺序,讲解四种主流解法:

  1. 暴力线性搜索:理解问题本质,但效率低下。
  2. 二分查找法:最优解法之一,重点掌握。
  3. 数学技巧法:利用完全平方数的数学性质。
  4. 牛顿迭代法:另一种高效解法,拓展思维。

3. 解法一:暴力线性搜索(理解思路)

这是最直接的思路:既然完全平方数num可以写成x * x的形式,那么我们从1开始,逐个尝试整数x,计算x * x是否等于num,直到x * x大于num

3.1 代码实现

// Java 实现 class Solution { public boolean isPerfectSquare(int num) { if (num < 0) return false; // 题目虽为非负,但保持健壮性 if (num == 0 || num == 1) return true; // 处理边界 for (long i = 1; i <= num; i++) { // 注意使用long防止溢出 long square = i * i; if (square == num) { return true; } else if (square > num) { break; // 一旦平方超过num,后续肯定更大,直接结束 } } return false; } }
# Python 实现 class Solution: def isPerfectSquare(self, num: int) -> bool: if num < 0: return False if num in (0, 1): return True i = 1 while i * i <= num: if i * i == num: return True i += 1 return False

3.2 复杂度分析与缺陷

  • 时间复杂度:O(√n)。在最坏情况下(例如num不是完全平方数),我们需要遍历到大约 √num 次。
  • 空间复杂度:O(1)。
  • 主要缺陷:效率太低。对于num=2147483647这样的最大输入,需要循环约 46340 次,在力扣上会超时。因此,暴力法仅适用于理解题意,不可作为最终答案

4. 解法二:二分查找法(推荐掌握)

这是本题的最优解和考点所在。思路是将搜索空间[1, num]视为一个有序序列(因为平方函数是单调递增的),在这个序列中查找是否存在一个数x,使得x * x == num

4.1 算法步骤

  1. 初始化:设置左边界left = 1,右边界right = num。对于num为 0 或 1 的情况可以提前处理。
  2. 循环条件:当left <= right时,执行循环。
  3. 计算中间值mid = left + (right - left) / 2务必使用此写法,而非(left+right)/2,以防止left+right溢出。
  4. 比较平方:计算mid * mid并与num比较。
    • mid * mid == num,找到目标,返回true
    • mid * mid < num,说明mid太小,目标在右侧,调整left = mid + 1
    • mid * mid > num,说明mid太大,目标在左侧,调整right = mid - 1
  5. 循环结束:若未找到,返回false

4.2 关键点:如何防止溢出?

这是二分法解本题的最大陷阱midintmid * mid很可能超过int的最大值(2147483647),导致溢出变成负数,进而引发判断错误和无限循环。

解决方案:

  • 使用long类型:将中间变量升级为long类型进行计算和比较。
  • 改变比较方式:不计算mid * mid,而是比较midnum / mid。但要注意处理mid为 0 的情况。

4.3 代码实现(防溢出版)

// Java 实现 (使用 long 防止溢出) class Solution { public boolean isPerfectSquare(int num) { if (num < 2) { return true; // 0 和 1 都是完全平方数 } long left = 1, right = num; // 使用 long 定义边界 while (left <= right) { long mid = left + (right - left) / 2; long square = mid * mid; // 用 long 存储平方 if (square == num) { return true; } else if (square < num) { left = mid + 1; } else { right = mid - 1; } } return false; } }
# Python 实现 (Python 整数自动支持大数,无需担心溢出) class Solution: def isPerfectSquare(self, num: int) -> bool: if num < 2: return True left, right = 1, num while left <= right: mid = left + (right - left) // 2 square = mid * mid if square == num: return True elif square < num: left = mid + 1 else: right = mid - 1 return False

4.4 复杂度分析

  • 时间复杂度:O(log n)。每次循环将搜索范围减半。
  • 空间复杂度:O(1)。
  • 优点:高效、稳定,是面试官最期望看到的解法。

5. 解法三:数学技巧法(利用奇数和)

这是一个有趣的数学性质:完全平方数都可以表示为从1开始的连续奇数的和

  • 1 = 1
  • 4 = 1 + 3
  • 9 = 1 + 3 + 5
  • 16 = 1 + 3 + 5 + 7

因此,我们可以从num中依次减去 1, 3, 5, 7... 如果最终能减到 0,说明num是完全平方数。

5.1 代码实现

// Java 实现 class Solution { public boolean isPerfectSquare(int num) { int odd = 1; while (num > 0) { num -= odd; odd += 2; // 下一个奇数 } return num == 0; // 如果刚好减到0,则是完全平方数 } }
# Python 实现 class Solution: def isPerfectSquare(self, num: int) -> bool: odd = 1 while num > 0: num -= odd odd += 2 return num == 0

5.2 复杂度分析

  • 时间复杂度:O(√n)。和暴力法类似,需要执行大约 √num 次减法。
  • 空间复杂度:O(1)。
  • 评价:代码非常简洁,体现了数学之美。但效率上不如二分法,且可能不如二分查找直观,适合作为知识拓展。

6. 解法四:牛顿迭代法(拓展思维)

牛顿迭代法是求方程根(例如x² - num = 0的根)的经典数值方法。对于本题,我们可以用它来快速逼近sqrt(num),然后判断其整数部分的平方是否等于num

迭代公式x_{n+1} = (x_n + num / x_n) / 2

6.1 算法步骤

  1. 初始猜测x0 = num(或num / 2)。
  2. 进行迭代:x = (x + num / x) / 2
  3. x * xnum的差值小于一个很小的阈值(例如 1)时,停止迭代。
  4. x的整数部分int(x),判断其平方是否等于num

6.2 代码实现

// Java 实现 class Solution { public boolean isPerfectSquare(int num) { if (num < 2) return true; long x = num / 2; // 初始猜测值 // 牛顿迭代 while (x * x > num) { x = (x + num / x) / 2; } // 检查迭代结果的平方是否等于num return (x * x == num); } }
# Python 实现 class Solution: def isPerfectSquare(self, num: int) -> bool: if num < 2: return True x = num // 2 while x * x > num: x = (x + num // x) // 2 return x * x == num

6.3 复杂度分析

  • 时间复杂度:O(log n),且收敛速度非常快。
  • 空间复杂度:O(1)。
  • 评价:效率很高,但理解起来需要一定的数学背景。在面试中可以作为展示你知识广度的备选方案。

7. 实战对比与测试用例

为了验证代码的正确性,我们需要设计全面的测试用例。

7.1 测试用例设计

输入 (num)预期输出说明
0true边界条件:0是0的平方
1true边界条件:1是1的平方
4true小的完全平方数
16true典型的完全平方数
14false非完全平方数
2147483647false最大整数输入,测试性能和溢出
808201true899的平方,较大的完全平方数
-1false无效输入(虽然题目约束非负,但健壮性考虑)

7.2 在力扣上的运行

将上述任何一种解法(推荐二分法)的代码提交到 LeetCode 367 题,应该能通过所有测试用例,并得到一个不错的运行时间和内存消耗排名。

8. 常见错误与排查思路

在解决本题时,新手常会遇到以下几个问题:

8.1 无限循环或错误结果(二分法)

  • 问题现象:代码在力扣上超时,或在某些测试用例(如大数)上返回错误答案。
  • 根本原因
    1. 整数溢出mid * mid使用int计算导致溢出,使得square < num的判断永远不成立或错误成立。
    2. 边界更新错误:在square < num时,错误地更新了right = mid - 1,导致错过解。
    3. 循环条件错误:使用while (left < right)但处理不当,可能提前退出循环。
  • 解决方案
    1. 强制使用long:这是最稳妥的方法。将所有与平方计算相关的变量(left,right,mid,square)声明为long
    2. 仔细检查更新逻辑:牢记“左移右减”的口诀:square < num时目标在右边,更新leftsquare > num时目标在左边,更新right
    3. 统一使用while (left <= right):这是标准的二分查找模板,易于理解和记忆。

8.2 忽略边界条件

  • 问题:未处理num = 0num = 1的情况,导致二分查找的初始区间[1, num]无效(当num=1时区间有效,但num=0right=0left=1,循环不会进入)。
  • 解决:在函数开头添加对num < 2的判断,直接返回true

8.3 数学技巧法的陷阱

  • 问题:对于非常大的非完全平方数,循环次数依然是 O(√n),可能存在效率问题(尽管通常能通过)。
  • 解决:理解其复杂度,知道这不是最优解。在面试中如果被问及效率,应能指出这一点。

9. 最佳实践与工程建议

将这道题的解法学透,不仅能解决当前问题,更能提升你解决一类问题的能力。

9.1 二分查找的通用模板

本题的二分查找是“在有序整数序列上查找特定条件”的典型应用。你可以总结一个模板:

public int binarySearchTemplate(int target) { int left = 下界, right = 上界; // 确定搜索范围 while (left <= right) { // 常用条件 int mid = left + (right - left) / 2; // 防溢出 if (满足条件(mid, target)) { return mid; // 或进行其他操作 } else if (某种比较(mid, target)) { left = mid + 1; // 调整左边界 } else { right = mid - 1; // 调整右边界 } } return -1; // 未找到 }

这个模板同样适用于“爱吃香蕉的狒狒”(LeetCode 875)、“在排序数组中查找元素的第一个和最后一个位置”(LeetCode 34)等问题。

9.2 类型选择与溢出防御

  • 默认使用long:在涉及乘法、加法可能溢出的场景,尤其是二分查找中计算中间值的平方时,优先考虑使用范围更大的数据类型(如long)。
  • 掌握防溢出计算:计算中点时,使用mid = left + (right - left) / 2而非(left + right) / 2

9.3 代码清晰与注释

即使是简单的算法题,清晰的代码结构也至关重要。

  • 为特殊逻辑(如边界处理)添加简短注释。
  • 变量名要有意义,如left,right,mid,square
  • 提前返回可以简化代码逻辑,减少嵌套。

9.4 关联题目与举一反三

完全掌握此题后,可以挑战以下关联题目,巩固二分查找和数学思维:

  • LeetCode 69. x 的平方根:本题的“姊妹题”,要求返回平方根的整数部分,解法几乎完全相同。
  • LeetCode 633. 平方数之和:判断一个数是否能表示为两个整数的平方和,可以使用双指针或二分查找。
  • LeetCode 279. 完全平方数:动态规划经典问题,求一个数最少能被多少个完全平方数相加得到。
  • LeetCode 875. 爱吃香蕉的狒狒:二分查找应用于“在满足条件的范围内寻找最小值”的典型。

10. 总结

LeetCode 367 “有效的完全平方数”是一道优秀的入门算法题,它像一块试金石,能检验出你对基础算法的掌握是否扎实。

核心收获:

  1. 二分查找是王道:对于在有序范围内查找满足特定单调条件的解,二分查找是首选,时间复杂度为 O(log n)。
  2. 细节决定成败:整数溢出是本题最大的陷阱,也是面试官考察你代码健壮性的关键点。使用long类型是简单有效的防御策略。
  3. 一题多解,开阔思路:从暴力法到二分法,再到数学法和牛顿法,每种解法都体现了不同的思维方式。掌握多种解法能让你在面试中游刃有余。
  4. 模板化与迁移能力:将二分查找抽象成模板,并理解其变体(如本题中搜索目标是“平方等于num的数”),能帮助你快速解决一系列相似问题。

在真正的面试或机考中(如华为OD),遇到此类题目,建议按照以下步骤快速解答:

  1. 确认输入输出和边界条件。
  2. 优先考虑二分查找解法。
  3. 在代码中显式处理溢出风险(使用long)。
  4. 用几个简单的测试用例(如0, 1, 4, 14)快速验证逻辑。
  5. 如果时间允许,可以简要提及其他解法以展示知识广度。

算法能力的提升源于对每一道基础题的深入思考和反复练习。希望这篇详细的解析能帮助你彻底攻克“有效的完全平方数”这道题,并将其中的思想应用到更广泛的算法学习中去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 2:45:44

顺丰科技大数据挖掘笔试复盘:高频考点与避坑指南

2019年那个秋天&#xff0c;我投了顺丰科技的大数据挖掘与分析工程师岗位。笔试刷下来最大的感受是&#xff1a;这套客观题不像一般互联网公司那样只考算法题或者纯机器学习理论&#xff0c;它把数据挖掘、统计学、大数据组件和业务分析逻辑全揉在了一张卷子里&#xff0c;覆盖…

作者头像 李华
网站建设 2026/9/6 18:15:35

ANSYS初级教程合集:零基础入门结构分析完整路径

这次我们来看一套 ANSYS 初级系列教程合集。它不是一个软件&#xff0c;也不是某个建模插件&#xff0c;而是一套面向零基础新手的系统性入门课程&#xff0c;由公众号 ANSYS 结构院整理发布。整套教程把 ANSYS 学习过程中最容易劝退的环节——软件怎么启动、工作目录怎么设置、…

作者头像 李华
网站建设 2026/9/7 0:35:21

Enscape 4.19实时渲染插件安装部署与性能优化全攻略

这次我们来看 Enscape 4.19。一句话介绍&#xff1a;Enscape 是挂在 SketchUp、Revit、Rhino 等建模软件上的实时渲染插件&#xff0c;建模过程中直接弹出一个渲染窗口&#xff0c;调材质、挪灯光、改视角全部实时更新。很多建筑可视化工作流里&#xff0c;Enscape 已经成了方案…

作者头像 李华
网站建设 2026/9/6 9:29:09

小红书Android校招笔试题深度解析:大厂考点与备战指南

1. 小红书2020校招Android方向笔试题解析&#xff1a;从一套题看大厂筛选逻辑先直接说结论&#xff1a;小红书这套Android校招笔试题&#xff0c;在2020年那个时间节点&#xff0c;放到今天来看依然很能打。它没有故意刁难人&#xff0c;也没有堆砌偏难怪题&#xff0c;而是非常…

作者头像 李华
网站建设 2026/9/6 23:07:01

python中def的用法 return_Python函数基础--def及return语句地操作

1def是可执行的代码函数借助有一个全新的语句得以编写, 即def。不同于C这种编译语言, def实际上是一条可执行语句—— 直至运行def后函数才存在, 函数本身一开始并不存在。对于典型操作而言, def语句于模块文件里编写, 并且在模块文件初次被导入之时, 自然而然地生成那些经定义…

作者头像 李华
网站建设 2026/9/5 22:25:32

Shell脚本保姆级教程:从基础语法到自动化运维实战

很多刚开始接触 Linux 的朋友&#xff0c;尤其是想转行运维的读者&#xff0c;都有类似的困惑&#xff1a;看到别人在终端里敲几行命令就能自动完成一堆任务&#xff0c;自己却还在一个个手动输入&#xff1b;写了几个命令&#xff0c;重启服务器后又得重新来一遍。其实这些问题…

作者头像 李华