问题描述
给定一个非负整数 x,请计算并返回它的 算术平方根(即 √x),结果只保留整数部分,小数部分将被舍去。
限制条件:不允许使用任何内置指数函数或运算符(如 Math.pow(x, 0.5) 或 x ** 0.5)。
一、初步思路:暴力枚举法
思路说明
逐个尝试整数 i,直到找到满足 i * i > x 的最小整数,此时 i - 1 即为答案。
JavaScript 实现
function mySqrt(x) {
if (x === 0 || x === 1) return x;
let i = 1;
while (i * i <= x) {
i++;
}
return i - 1;
}
时间复杂度分析
- 最坏情况下需要遍历到
√x,时间复杂度为 O(√x)。 - 对于大数(如
x = 2^31 - 1),效率较低。
二、优化方法一:二分查找法
核心思想
平方根的值一定在 0 到 x 之间。通过 二分查找 缩小范围,每次将搜索区间减半,从而快速逼近答案。
JavaScript 实现
function mySqrt(x) {
if (x < 2) return x;
let left = 0, right = x;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
// 使用 mid <= x / mid 避免整数溢出
if (mid <= x / mid) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return right;
}
时间复杂度分析
- 每次将搜索空间减半,时间复杂度为 O(log x)。
- 显著优于暴力法,适用于非常大的输入值。
优化点
- 避免整数溢出:使用
mid <= x / mid代替mid * mid <= x。 - 边界处理:当
x < 2时直接返回x。
三、进一步优化:牛顿迭代法
数学背景
牛顿迭代法是一种数值逼近方法,用于求解方程 f(x) = 0 的近似解。对于 x² = n,我们可以通过迭代公式逐步逼近 √n。
迭代公式推导
设 f(x) = x² - n,其导数为 f’(x) = 2x。
根据牛顿迭代法的公式:
JavaScript 实现
function mySqrt(x) {
if (x < 2) return x;
let guess = x;
while (true) {
const newGuess = Math.floor((guess + x / guess) / 2);
if (newGuess >= guess) {
return guess;
}
guess = newGuess;
}
}
时间复杂度分析
- 牛顿法的收敛速度为 二次收敛,即每次迭代的误差平方级减少,因此在实践中非常高效。
- 时间复杂度接近 O(log log x),远优于二分查找。
优势与局限性
- 优势:收敛速度快,适合高精度计算。
- 局限性:需要理解数学原理,且对初始值选择敏感(但此处固定初始值
x已足够)。
五、方法对比与总结
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 暴力枚举法 | O(√x) | O(1) | 小规模输入 | 实现简单 | 效率低 |
| 二分查找法 | O(log x) | O(1) | 中等规模输入 | 效率高,代码简洁 | 需处理边界条件 |
| 牛顿迭代法 | O(log log x) | O(1) | 大规模输入或高精度需求 | 收敛速度快,效率极高 | 需数学推导,实现稍复杂 |
推荐解法
- 常规场景:优先使用 二分查找法,因其代码简洁、时间复杂度低。
- 高性能需求:使用 牛顿迭代法,尤其适合对精度要求极高的场景(如科学计算)。
六、扩展思考
-
如何避免整数溢出?
在二分查找中,使用mid <= x / mid代替mid * mid <= x,可避免大数相乘导致溢出。 -
如何处理浮点数精度问题?
牛顿迭代法中,使用Math.floor((guess + x / guess) / 2)代替浮点数除法,确保结果符合题目要求。 -
其他方法:
- 袖珍计算器算法:通过
Math.exp(0.5 * Math.log(x))计算平方根,但可能因浮点数精度损失导致错误,需额外修正。
- 袖珍计算器算法:通过