Leetcode 69——不使用sqrt函数情况下求平方根整数部分(暴力求解法和二分查找法)
文章目录
- 不使用sqrt函数情况下求平方根整数部分
- 暴力求解
- 解题思路
- 代码实现
- int的上溢出风险
- 利用不等式性质调整边界
- 强制类型转换到更长字节的整形
- 时间复杂度和空间复杂度
- 二分查找
- 算法思路
- 代码实现
- 时间复杂度和空间复杂度
不使用sqrt函数情况下求平方根整数部分
本篇文章是对Leetcode 69题目进行记录
现在提供以下两种思路:
- 暴力求解
- 二分查找
以下我们将对这两种算法进行记录,原题链接:
Leetcode 69.x的平方根
暴力求解
几乎所有的题,我们第一时间都想到的是能不能暴力求解,先不管时间复杂度的要求。当然这题是没有要求的,我们可以先试着写一下暴力求解的思路。
解题思路
因为只需要求的是整数x(int 类型)的平方根的整数部分,使用暴力求解是非常简单的。定义int i = 1;只需要让i从1开始遍历到x,i2只要小于等于x就继续往下走,直到大于的时候返回i - 1这个值即可。思路十分简单。
代码实现
int的上溢出风险
class Solution {
public:
int mySqrt(int x) {
if (!x)
return x;
int sqrtx = 1;//专门用来接收平方根的整数部分
for (int i = 1; i <= x; i++) {
if (i * i <= x)
sqrtx = i;
else
break;
}
return sqrtx;
}
};
很多人会不假思索地写出这么一段代码。但是忽略了一个问题就是上溢出的风险。
对于int类型,其实是unsigned int类型,是有符号整形,最大值应该是231 - 1 = 2147483647才对。当i从1开始遍历,一开始i比较小,那么i2的值肯定是可以用int接收的(本身i * i都是int类型,算出来也是int类型)。
但是我们尝试着跑一下这个代码:
执行是出错的,看到错误原因是当i = 46341的时候,i * i的值是无法作为int类型数据和比较的,所以得进行修改代码。
利用不等式性质调整边界
通过计算器计算得知:46431 * 46431 = 2155837761是大于int类型数据的最大值的。也就是说,对于x来讲,无论多大,只要是再int范围内,最多i只需要遍历到46431就会停下,只不过这个时候会导致上溢出风险。
我们学过不等式:
if a < b
then a * c < b * c (c != 0)
既然只有i = 46431的时候会导致平方越界,那么两边同时乘一个0.5不就好了,这样子还会把计算后的结构类型提升到double类型,就可以比较了,一定不会越界:
class Solution {
public:
int mySqrt(int x) {
if (!x)
return x;
int sqrtx = 1;//专门用来接收平方根的整数部分
for (int i = 1; i <= 0.5 * x; i++) {
if (0.5 * i * i <= 0.5 * x)
sqrtx = i;
else
break;
}
return sqrtx;
}
};
强制类型转换到更长字节的整形
这个前提是题目没有要求说题目中不给使用64位的整形数据。
既然int比较不了,那直接类型提升到long long不就可以了吗?
class Solution {
public:
int mySqrt(int x) {
if (!x)
return x;
int sqrtx = 1;
for (int i = 1; i <= 0.5 * x; i++) {
if ((long long)i * i <= x)
sqrtx = i;
else
break;
}
return sqrtx;
}
};
时间复杂度和空间复杂度
我们看看经过修改后的代码(调整边界方法时间和这个差不多)
时间复杂度为O(LogX),其实很好理解,如果x比较大,那么跑的次数会非常接近刚刚的那个数46431,其实也就是x的平方根整数部分,那效率还是比较低的。
空间复杂度为O(1),因为该算法没有另外开辟空间。
二分查找
既然暴力求解是从1开始找到x,那么就是在一个有序的序列上找一个确定的数,那么我们可以尝试一下使用二分查找。
算法思路
我们可以尝试一下二分查找。刚刚我们也知道int类型数据内找平方根最大也就到46431就会越界,所以我们可以控制一下查找的范围,就定在1~50000即可。
二分查找就是在1~50000中查找:
定义一个变量上界为int up ,下届为int down。设中间数为int mid。
让mid = (up + down) / 2。开始查找:
如果当前mid * mid的值和x相同,那就返回 mid这个值。
如果小于就让mid为下界,即赋值给down,重新计算mid
如果大于就让mid为上界,即赋值给up,重新计算mid
但是不是所有的数都能找到整数平方根,但是二分查找会不断压缩直到只有一个数,此时mid的值和down是一样的,那就直接停止循环即可。
但是每个数字都从50000开始为上界还是太慢了。就比如我要找10的平方根,从10开始找才是更快的,从50000开始为上界那就不太行了。但是int所有类型的数据最大的平方根也不超过50000,所以我们做一个规定,如果传入的x小于等于50000,就让上界为x,反之为50000。
我们以60作为例子:
很明显,这个思路是可行的。现在还有一个问题就是对于0这个数是否也能用这个逻辑呢?
如果x == 0,上界为0,下界为1,那么中间数就是0。然后当前中间数偏小,就让0为下届,原本的上界0仍是上界,再计算此时的mid也是0。还是偏小。但是此时mid和down的值相同,直接退出循环就可以了。所以也是可以成功的。
代码实现
class Solution {
public:
int mySqrt(int x) {
// 此时mid大约为2.9w
int up = (x <= 50000) ? x : 50000, down = 1;
int mid = (up + down) / 2;
while (mid != down) {
// 乘0.5是防止越界int的范围
if (0.5 * mid * mid == 0.5 * x)
return mid;
else if (0.5 * mid * mid > 0.5 * x)
up = mid;
else
down = mid;
mid = (up + down) / 2;
}
return mid;
}
};
当然还是需要注意上溢出风险的。具体处理方法可以参照上个部分讲的两种解决方案。
时间复杂度和空间复杂度
很明显空间复杂度仍是O(1),因为没有另外开辟空间进行操作。
时间复杂度仍为O(LogX),这个看着和上面那个算法的O(LogX)好像没什么区别,实则不然。
虽然二者查找次数均为LogX次,但是也是有区别的。特别数字比较大的时候。当数字较大,暴力查找是要从1到x的平方根跑满的,而二分查找却不用那么多次。
就比如找90000的平方根,暴力查找要找300次,而二分查找只需要14次(大致计算),这个效率差是非常大的。