leetcode 69. x的平方根

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..., 由于返回类型是整数,小数部分将被舍去。

一、 牛顿迭代法


牛顿迭代法


leetcode 69. x的平方根_第1张图片
背景:多数方程不存在求根公式,因此求精确根非常困难,甚至不可解,从而寻找方程的近似根就显得特别重要。牛顿迭代法是求方程根的重要方法之一

可证明,在无法直接求出 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 nlimxn=b


具体过程
leetcode 69. x的平方根_第2张图片
1、确定 f ( x ) f(x) f(x):这里设为 f ( x ) = x 2 − a f(x) = x^2 -a f(x)=x2a

2、确定精度 ϵ \epsilon ϵ

3、目的:求解使得 f ( x ) = x 2 − a = 0 f(x) = x^2 -a = 0 f(x)=x2a=0第一个满足精度的近似解 x n x_n xn

4、迭代条件:

  • 迭代次数确定,可以计算出来:构建一个固定次数的循环
  • 迭代次数无法确定:具体问题具体分析,如求a的平方根使用精度来确定何时终止迭代
    • 采用相邻两次得到的x的差距:每一次迭代后,我们都会距离零点更进一步,当相邻两次迭代得到的交点非常接近时,我们就可以断定,此时的结果已经足够我们得到答案了。一般来说,可以判断相邻两次迭代的结果的差值是否小于一个极小的非负数 ϵ \epsilon ϵ,其中 ϵ \epsilon ϵ 一般可以取 1 0 − 6 / 7 10^{-6/7} 106/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=xn1f(xn1)f(xn1)


python


时间复杂度 O ( log ⁡ N ) O(\log N) O(logN):此方法是二次收敛的,比二分查找更快证明:leetcode 69. x的平方根_第3张图片

空间复杂度 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=xn1f(xn1)f(xn1)带入化简得到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、可能溢出的通用模版


注意点:

  1. 不考虑具体的 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=xn1f(xn1)f(xn1)是很可能溢出的
  2. 为了更具备通用性,最后可以自己设置返回的近似根的小数点位数
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 x2a的最大元素。

注意点

  1. 本题如果要求返回小数的话就不能用二分法了
  2. 要单独考虑a = 0的情况,否则二分法部分为了防溢出写成if mid <= a / mid形式,mid就会=0
  3. 二分法两个溢出考虑
    • mid = left + (right - left) // 2
    • if mid <= a / mid,mid^2可能溢出
  4. 二分在[1,a]做,搜索能使得 x 2 ≤ a x^2 \leq a x2a最大元素,举例可知最终right会指向结果数值
  5. 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

你可能感兴趣的:(#,二分法,#,牛顿迭代法,leetcode,算法,python)