Q62- code69- x 的平方根
实现思路
1 二分查找法
- 对于这种 极值边界极端情况,要么 提前收缩范围,要么 处理过程中 变乘法为除法
- 开区间 + 求 <=的 最大值:一般是返回l
- 开区间 + 求 >=的 最小值:一般是返回r
2 牛顿迭代法
S1 转化题意
- 我们要找一个数r,使得 r^2 = t
- 可以转化为 找方程 f(x)= x^2 - t = 0的解
- 这就是说,我们要找到一个正实数x,使得 x^2 - t = 0
S2 理解导数
- 对于函数 f(x)= x^2 - t
- 它的导数 f'(x) = 2x
- 导数表示函数在每个点的斜率
S3 在某一点做切线
- 假设我们在点 x0处, 这个点的y0是 x0^2 - t
- 这个点的 斜率/导数 是2x0
S4.1 写出 切线方程
- y-yo = k(x - x0)
- 即 y- (x0^2 - t) = 2x(x - x0)
S4.2 求解 正实数根,此时 y = 0
- 即 0-(0^2 - t) = 2x0(x - x0) ==> -x0^2 + t = 2x0x - 2x0 ^ 2 ==> t = 2x0x - x0 ^ 2 ==> 2x0 x = x0 ^ 2 + t ==> x = (x0 ^ 2 + t) / 2x0 ==> x = (x0 + t/x0) / 2
S6 理解结果
-
这个x 就是 切线与x轴的交点
-
它比 原来的 x0更接近真实的平方根
-
这就是我们迭代公式:x = (x0 + t/x0) / 2
-
如果 x0正好是平方根,那么 t = x0 ^ 2
-
代入公式:(x0 + t/x0)/2 = (x0 + x0) / 2 = x0
-
说明如果找到了正确的值,这个值就不会再变化
参考文档
代码实现
1 方法1- 二分查找法
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
function mySqrt(x: number): number {
// 46340是 2^31 - 1 的平方根
let l = 0, r = Math.min(x, 46340) + 1;
while (l + 1 < r) {
const mid = l + ((r - l) >> 1);
mid * mid <= x ? (l = mid) : (r = mid);
}
return l;
}
2 方法2- 牛顿迭代法
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
function mySqrt(x: number): number {
if (x === 0 || x === 1) return x;
// 选择更好的初始值:对于大数,可以用 x/2; 对于小数,可以用 x
let diff = x > 4 ? x / 2 : x, t = x;
// 易错点1: 循环条件应该是 diff * diff - t > 预期精度值
while (Math.abs(diff * diff - t) > 0.00001) {
diff = (diff + t / diff) / 2;
}
return ~~diff;
}
变形题目
1 求x的平方根,精确到小数点后n位
思路解析
1 为什么要 -(n + 1) 而不是 -n
- 如果要求精确到小数点后 3 位
- 那么误差必须小于 0.0001(比第3位更小一位)
- 这样才能保证四舍五入后的结果是正确的
2 为什么 最后返回时需要 parseFloat(((l + r) / 2).toFixed(n))
- 当循环结束时,说明 r - l ≤ eps(区间足够小了)
- 此时 l 和 r 非常接近:l 通常会稍微小于真实值,r 会稍微大于真实值
- 取 平均值 能得到最接近真实值的结果
代码实现
const mySqrt = (x, n) => {
if (x === 0 || x === 1) return x;
// 初始化左右边界, 优化右边界初始值
let l = 0, r = x > 1 ? x : 1;
// Math.pow(10, -1) = 0.1
// 计算精度:小数点后n位意味着误差要小于10^(-n-1)
let eps = Math.pow(10, -(n + 1));
// 二分搜索
while (r - l > eps) {
const mid = l + ((r - l) >> 1)
mid * mid < x ? l = mid : r = mid;
}
// 取平均结果,减少误差
// .toFixed(n) 保留指定小数位数(返回字符串)
// parseFloat() 将字符串转换为浮点数
return parseFloat(((l + r) / 2).toFixed(n));
};