LeetCode —— 69. x 的平方根

160 阅读3分钟

启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

该题是数组二分查找题型第四题。

数组专栏 - 掘金

题目来源

69. x 的平方根(LeetCode)

题目描述(简单

给你一个非负整数 x ,计算并返回 x算数平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

示例1

输入: x = 4
输出: 2

示例2

输入: x = 8
输出: 2
解释: 8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

提示

  • 0<=x<=23110 <= x <= 2 ^ {31} - 1

题目解析

该题有两种解题思路:

  • 通过其它的数学函数代替平方根函数得到精确结果,取整数部分作为答案
  • 通过数学方法得到近似结果,直接作为答案

该题作为二分查找的巩固训练,先采用二分查找的方法来得到结果。

二分查找

由于 x 的平方根整数部分是满足 num2<=xnum^2 <= x 的最大值 num ,因此我们对 num 进行二分查找即可。

于是,可以设定左边界为 0 ,右边界为 x ,在二分查找中,不断的比较 midx 的关系。

当你举例够多的时候,你会发现,在 0<=x<=23110 <= x <= 2 ^ {31} - 1 范围中, (x2)2>x (\frac{x}{2})^2 > x 必定成立,所以可以将右边界设定为 x2\frac{x}{2}

代码

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    let left = 1, right = Math.ceil(x / 2)
    while (right >= left) {
        let mid = Math.floor((left + right) / 2)
        if (mid * mid > x) {
            right = mid - 1
        }else if (mid * mid < x) {
            left = mid + 1
        }else{
            return mid
        }
    }
    return right
};
  • 时间复杂度:O(logx)O(\log x)
  • 空间复杂度:O(1)O(1)

如图:

image.png

当等于和小于合并为一个判断条件时,你会发现, left - 1 一直是满足 num * num <= x 的最大值。

代码(精简代码)

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    let left = 1, right = Math.ceil(x / 2)
    while (right >= left) {
        let mid = Math.floor((left + right) / 2)
        if (mid * mid > x) {
            right = mid - 1
        }else {
            left = mid + 1
        }
    }
    return left - 1
};

如图:

image.png

而当等于和大于合并时,你会发现 right 有两种情况:

  • right * right = x
  • (right + 1) * (right + 1) = x

因此需要对 rightright + 1 进行判断。

代码(精简代码)

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    let left = 1, right = Math.ceil(x / 2)
    while (right >= left) {
        let mid = Math.floor((left + right) / 2)
        if (mid * mid >= x) {
            right = mid - 1
        }else {
            left = mid + 1
        }
    }
    return (right + 1) * (right + 1) <= x ? right + 1 : right
};

如图:

image.png

总而言之,该题需要找到满足 num * num <= x 的最大值 num 。当 num * num === x 时,left - 1right + 1 都可以作为返回结果。但是当 num * num < x 时,在最后一次循环 left === right 中,根据 mid * mid < x ,所以只会执行 left = mid + 1 该条语句,所以将等于的情况合并到小于的情况中时,返回结果不需要变化,但将等于的情况合并到大于的情况中时,返回结果就需要判断了。

袖珍计算器算法

该方法是一种用指数函数 expMath.exp() - JavaScript )和对数函数 lnMath.log1p() - JavaScript )代替平方根函数的方法。通过有限的可以使用的数学函数,得到计算的预期结果。

公式如下:

x=(elnx)12=e12lnx\sqrt{x} = (e ^ {\ln x}) ^ {\frac{1}{2}} = e ^ {\frac{1}{2}\ln x}

注意:由于计算机无法存储浮点数的精确值(浮点数的存储方法可以参考 IEEE 754_百度百科) ,这里不在赘述。而指数函数和对数函数的参数和返回值均为浮点数,因此计算过程中会存在误差。例如当 x = 2147395600 时, e12lnxe ^ {\frac{1}{2}\ln x} 的运算结果与正确值 46340 相差 101110 ^ {-11} ,这样在对结果取整数部分时,会得到 46339 这个错误结果,因此得到整数部分,应当找出 res + 1res 两个中哪一个是正确答案。

代码

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    if (x === 0) {
        return 0
    }
    let res = Math.floor(Math.exp(0.5 * Math.log1p(x - 1)))
    return (res + 1) * (res + 1) <= x ? res + 1 : res
};
  • 时间复杂度:O(1)O(1),由于内置的 exp 函数与 log 函数一般都很快,所以这里将其复杂度视为 O(1) 。
  • 空间复杂度:O(1)O(1)

如图:

image.png

执行用时和内存消耗仅供参考,大家可以多提交几次。如有更好的想法,欢迎大家提出。