69. x的平方根
给你一个非负整数 x(0 <= x <= 231 - 1) ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
一、 牛顿迭代法
牛顿迭代法
背景:多数方程不存在求根公式,因此求精确根非常困难,甚至不可解,从而寻找方程的近似根就显得特别重要。牛顿迭代法是求方程根的重要方法之一
可证明,在无法直接求出
f
(
x
)
f(x)
f(x)的根
b
b
b的时候,可通过不断迭代得到无限接近与
b
b
b的根
x
n
x_n
xn:
lim
n
→
∞
x
n
=
b
{{\lim_{n\to ∞}}}x_n = b
n→∞limxn=b
具体过程:
1、确定
f
(
x
)
f(x)
f(x):这里设为
f
(
x
)
=
x
2
−
a
f(x) = x^2 -a
f(x)=x2−a
2、确定精度 ϵ \epsilon ϵ
3、目的:求解使得 f ( x ) = x 2 − a = 0 f(x) = x^2 -a = 0 f(x)=x2−a=0的第一个满足精度的近似解 x n x_n xn
4、迭代条件:
- 迭代次数确定,可以计算出来:构建一个固定次数的循环
- 迭代次数无法确定:具体问题具体分析,如求a的平方根使用精度来确定何时终止迭代
- 采用相邻两次得到的x的差距:每一次迭代后,我们都会距离零点更进一步,当相邻两次迭代得到的交点非常接近时,我们就可以断定,此时的结果已经足够我们得到答案了。一般来说,可以判断相邻两次迭代的结果的差值是否小于一个极小的非负数 ϵ \epsilon ϵ,其中 ϵ \epsilon ϵ 一般可以取 1 0 − 6 / 7 10^{-6/7} 10−6/7
- 采用相邻两次得到的 f f f的差距:计算量更大
5、迭代关系式: x n = x n − 1 − f ( x n − 1 ) f ′ ( x n − 1 ) x_{n} = x_{n-1} - {f(x_{n-1})\over f'(x_{n-1})} xn=xn−1−f′(xn−1)f(xn−1)
python
时间复杂度 O ( log N ) O(\log N) O(logN):此方法是二次收敛的,比二分查找更快证明:
空间复杂度 O ( 1 ) O(1) O(1)
1、不溢出版本
不溢出的原因在于,我们结合了平方根的具体题目:
- 初始单独考虑a = 0,防止后面作为分母
- 对公式:
x
n
=
x
n
−
1
−
f
(
x
n
−
1
)
f
′
(
x
n
−
1
)
x_{n} = x_{n-1} - {f(x_{n-1})\over f'(x_{n-1})}
xn=xn−1−f′(xn−1)f(xn−1)带入化简得到
next_x = 0.5 * (cur_x + a/cur_x)
,否则必然要计算 f f f,即 x 2 x^2 x2; - 采用相邻两次 x x x的精度差,若采用f精度差,则计算 f f f还是要算 x 2 x^2 x2
该解法我令a = -x,是为了表示,牛顿迭代初始x的选取,会决定你最后得到平方根x的正负号,如果题目规定返回正数,则最后我们还是要加个绝对值省点心哈。
执行用时:40 ms, 在所有 Python3 提交中击败了79.28% 的用户
内存消耗:14.9 MB, 在所有 Python3 提交中击败了61.01% 的用户
class Solution:
def mySqrt(self, x: int) -> int:
# 单独处理x =0,防止后面0作为分母被除
if x == 0:
return 0
a, cur_x, epsilon = x, -x, 1e-7
while True: # 精度满足则break
# 直接用化简公式则不用计算cur_x的平方,防止溢出
next_x = 0.5 * (cur_x + a/cur_x)
# 这里精度用的是x之间的精度差,而不是f函数值的精度差,用f还要计算平方,会溢出
if abs(next_x - cur_x) < epsilon:
break
cur_x = next_x
return int(abs(cur_x))
2、可能溢出的通用模版
注意点:
- 不考虑具体的 f f f进行化简,定义 f , f ′ f,f' f,f′函数,直接套 x n = x n − 1 − f ( x n − 1 ) f ′ ( x n − 1 ) x_{n} = x_{n-1} - {f(x_{n-1})\over f'(x_{n-1})} xn=xn−1−f′(xn−1)f(xn−1)是很可能溢出的
- 为了更具备通用性,最后可以自己设置返回的近似根的小数点位数
class Solution:
def mySqrt(self, x: int) -> int:
# 单独处理x =0,防止后面0作为分母被除
if x == 0:
return 0
def f(x):
return x ** 2 - a
def df(x):
return 2 * x
a, cur_x, epsilon = x, -x, 1e-7
while True: # 精度满足则break
next_x = cur_x - f(cur_x) / df(cur_x)
# 这里精度用的是x之间的精度差,而不是f函数值的精度差,用f还要计算平方,会溢出
if abs(next_x - cur_x) < epsilon:
break
cur_x = next_x
return float(format(abs(cur_x), "0.3f"))
>>> mySqrt(8)
2.828
3、非while True写法
这里采用f之间的精度差,没采用while True写法,避免了不单独考虑x=0,分母会除0的情况。
执行用时:36 ms, 在所有 Python3 提交中击败了91.88% 的用户
内存消耗:14.9 MB, 在所有 Python3 提交中击败了58.91% 的用户
class Solution:
def mySqrt(self, x: int) -> int:
a = x # 改成求a的平方根,近似值用x估计:f(x) = x^2 - a
def f(x):
return x ** 2 - a
def df(x):
return 2 * x
epsilon = 10 ** (-4) # 自己设置个精度,本文其实0就可以
cur_x = a # 初始x_0,随意选取f = x0^2 - a > epsilon即可
# 牛顿迭代法
while abs(f(cur_x)) - 0 > epsilon: # 不取等号
next_x = cur_x - f(cur_x) / df(cur_x)
cur_x = next_x
return int(cur_x) # 本文要求取整
二、二分查找:返回值取整的情况
思路
把找a的平方根问题,转化为:在[0, …, a]这一有序整数数组中,搜索能使得 x 2 ≤ a x^2 \leq a x2≤a的最大元素。
注意点:
- 本题如果要求返回小数的话就不能用二分法了
- 要单独考虑a = 0的情况,否则二分法部分为了防溢出写成
if mid <= a / mid
形式,mid就会=0 - 二分法两个溢出考虑:
mid = left + (right - left) // 2
if mid <= a / mid
,mid^2可能溢出
- 二分在[1,a]做,搜索能使得 x 2 ≤ a x^2 \leq a x2≤a的最大元素,举例可知最终right会指向结果数值
if mid <= a / mid
: 必须取等号,否则都不符合搜索本身条件了
python
时间复杂度 O ( log N ) O(\log N) O(logN):二分搜索次数
空间复杂度 O ( 1 ) O(1) O(1)
执行用时:40 ms, 在所有 Python3 提交中击败了79.28% 的用户
内存消耗:14.8 MB, 在所有 Python3 提交中击败了92.07% 的用户
class Solution:
def mySqrt(self, x: int) -> int:
# 1、主要针对x=0如果left从0开始搜索会出现mid=0作为分母的情况
if x <= 1: return x
# 2、二分查找:在 [1, a]搜索使得x^2 <= a 的最大x
a = x
left, right = 1, a
while left <= right:
mid = left + (right - left) // 2
if mid <= a / mid: # m^2 <= a 可能溢出;必须取等号
left = mid + 1 # 当前mid符合条件,试图找更大的x
else:
right = mid - 1 # 当前mid不符,则搜索更小的x
return right # 举例可知right最终指向result
总体写下两方法
class Solution:
def mySqrt(self, x: int) -> int:
if x <= 1: return x
# return self.newton(x)
return self. binary_search(x)
def newton(self, a):
cur_x, epsilon = a, 1e-7
while True:
next_x = 0.5 * (cur_x + a / cur_x)
if abs(cur_x - next_x) <= epsilon:
break
cur_x = next_x
return abs(int(cur_x))
def binary_search(self, a):
# 在[2, a]寻找最大的使得 x^2 <= a的x
left, right = 2, a
while left <= right:
mid = left + (right - left) // 2
if mid <= a / mid: # 必须取等号
left = mid + 1
else:
right = mid - 1
return right