Easy | LeetCode 69. x 的平方根 | 袖珍计算器 | 二分法 | 牛顿迭代法
69. x 的平方根
实现 int sqrt(int x)
函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
由于返回类型是整数,小数部分将被舍去。
解题思路
本题应该还加一个要求, 不允许直接开根号得出结果
方法一: 袖珍计算器算法
\[\sqrt{x}=x^{1 / 2}=\left(e^{\ln x}\right)^{1 / 2}=e^{\frac{1}{2} \ln x}
\]
根据以上公式, 可以替代直接根号的运算。但是这样计算的结果, 可能会带有误差。有精度丢失导致值偏小的可能性。所以在返回结果时, 需要把ans+1拿来尝试一次。
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
int ans = (int) Math.exp(0.5 * Math.log(x));
return (long) (ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
}
方法二: 二分法
找整数x, x+1,使得x ^ 2 <= n
并且(x+1)^2 > n
。可使用二分的方法来查找。二分的初始下界是0, 初始上界是N。
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
方法三: 牛顿迭代法
如图, 不断得用切线与X轴的交点为基础做切线迭代, 用于X轴的交点不断去逼近结果。
具体的公式推导如下:
我们选择 \(x 0=C\) 作为初始值。
在每一步迭代中, 我们通过当前的交点 \(x_{i},\) 找到函数图像上的点 \(\left(x_{i}, x_{2}^{2}-C\right),\) 作一条斜率为 \(f^{\prime}\left(x_{i}\right)=2 x_{i}\) 的直线, 直线的方程为:
\[\begin{aligned}
y l &=2 x i\left(x-x_{i}\right)+x_{2}^{2}-C \\
&=2 x i x-\left(x_{i}^{2}+C\right)
\end{aligned}
\]
与横轴的交点为方程 \(2 x i x-\left(x_{2}^{2}+C\right)=0\) 的解, 即为新的迭代结果 \(x_{i+1}:\)
\[x_{i+1}=\frac{1}{2}\left(x_{i}+\frac{C}{x_{i}}\right)
\]
在进行 \(k\) 次迭代后, \(x_{k}\) 的值与真实的零点 \(\sqrt{C}\) 足田接近, 即可作为答案。
这种方法有几点需要明白:
为什么选择 \(x 0=C\) 作为初始值?
- 因为 \(y=x^{2}-C\) 有两个零点 \(-\sqrt{C}\) 和 \(\sqrt{C}\) 。如果我们取的初始值较小,可能会迭代到 \(-\sqrt{C}\) 这 个零点, 而我们希望找到的是 \(\sqrt{C}\) 这个零点。因此选择 \(x 0=C\) 作为初始值,每次迭代均有 \(x_{i+1}<x_{i},\) 零点 \(\sqrt{C}\) 在其左侧, 所以我们一定会迭代到这个零点。
- 迭代到何时才算结束?
每一次迭代后, 我们都会距离零点更进一步,所以当相邻两次迭代得到的交点非常接近时, 我们 就可以断定, 此时的结果已经足够我们得到答案了。一般来说, 可以判断相邻两次迭代的结果的 差值是否小于一个极小的非负数 \(\epsilon,\) 其中 \(\epsilon\) 一般可以取 \(10^{-6}\) 或 \(10^{-7}\) 。 - 如何通过迭代得到的近似零点得出最终的答案?
由于 \(y=f(x)\) 在 \([\sqrt{C},+\infty]\) 上是凸函数 (convex function) 且恒大于等于零,那么只要我们选取 的初始值 \(x 0\) 大于等于 \(\sqrt{C},\) 每次迭代得到的结果 \(x_{i}\) 都会恒大于等于 \(\sqrt{C}\) 。因此只要 \(\epsilon\) 选择地足 句小, 最终的结果 \(x k\) 只会稍稍大于真正的零点 \(\sqrt{C}\) 。在题目给出的 32 位整数范围内,不会出现 下面的情况:
真正的零点为 \(n-1 / 2 \epsilon,\) 其中 \(n\) 是一个正整数, 而我们迭代得到的结果为 \(n+1 / 2 \epsilon_{\circ}\) 在对结 果保留整数部分后得到 \(n,\) 但正确的结果为 \(n-1_{0}\)
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
double C = x, x0 = x;
while (true) {
double xi = 0.5 * (x0 + C / x0);
if (Math.abs(x0 - xi) < 1e-7) {
break;
}
x0 = xi;
}
return (int) x0;
}