问题:为 int sqrt(int x)实现整数平方根
有很多方法可以计算整数平方根。但是如果你不打扰,你总是可以欺骗编译器(在线判断),但不要在你的面试中这样做!
作弊方式:
好吧,大多数编程语言都有 用于浮点数(单精度或双精度)的sqrt,我们可以简单地编写:
#include <math.h>
class Solution {
public:
int sqrt(int x) {
// add std:: namespace to distinguish from current sqrt()
return (int)std::sqrt(x);
}
};
或者
public class Solution {
public int sqrt(int x) {
return (int)Math.sqrt(x);
}
}
或者
()
#include <math.h>
class Solution {
public:
int sqrt(int x) {
return (int)pow(x, 0.5);
}
};
穷举搜索:
人们可能会很快想出一个蛮力搜索。我们只需要尝试从 1 开始的数字,直到这个数字的平方大于输出。
#include <math.h>
typedef unsigned long long ULONG;
class Solution {
public:
int sqrt(int x) {
ULONG a = 1;
while (a * a <= x) a ++;
return --a;
}
}
请注意,我们定义了一个unsigned long long类型来保存a * a 的中间结果, 这样结果就不会溢出(对于 32 位整数来说太大了)。
好吧,以上似乎不太可能在网上判断中接受。但令人惊讶的是,这是一个公认的解决方案。这是为什么?的 int 类型是32位有符号整数,其给出的最大值2 ^ 31 - 1 = 2147483647。这个的平方根只是 46340.9,这意味着最多46341次迭代,我们有正确的整数根。这在现代处理器中是微不足道的,可以在百万秒内完成。
改进:
我们注意到 a * a 很费计算资源,如何摆脱它?
因此,我们只需将2 添加到变量 delta 并将其添加到当前变量 a 即可得到a + 1的平方 。
#include <math.h>
typedef unsigned long long ULONG;
class Solution {
public:
int sqrt(int x) {
ULONG a = 1;
ULONG delta = 3;
while (a <= x) {
a += delta; // (a + 1)^2
delta += 2;
}
return delta / 2 - 1;
}
}
上述改进去除了乘法并仅使用加法,这通常在一些时间不是关键问题的 PC 板中实现。
二分查找:
上面的算法复杂度是O(n)并且当给定的整数很大时它仍然很慢。二分查找可以将其缩短为
二分搜索是最重要的算法之一,为了使其工作,它需要连续的搜索空间(例如数字排序)
在整数平方根的域中,我们可以很容易地发现:
小于
任何给定的非负整数。
#include <math.h>
typedef unsigned long long ULONG;
class Solution {
public:
int sqrt(int x) {
ULONG l = 0, r = x;
while (l <= r) {
ULONG mid = l + (r - l) / 2; // (l + r) / 2;
ULONG midmid = mid * mid;
// check if x falls into [mid^2, (mid + 1)^2)
if ((midmid <= x) && (x < (mid + 1) * (mid + 1))) return mid;
if (midmid > x) {
r = mid - 1;
} else {
l = mid + 1;
}
}
}
}
使用二分搜索,每次迭代都会将搜索空间缩小到一半。的 中期=(L + R)/ 2 可能溢出所以我们可以使用 中间= 1 +(R - 1)/ 2 来完成相同的事情。
牛顿法:
二分搜索是一个很大的改进,但找到解决方案仍然太慢。现代处理器实现了硬件平方根算法,其中大部分基于牛顿方程:
正如我们所看到的,随着牛顿方程的每次迭代,我们找到了更接近的根
我们有初步的猜测
。
例如,
牛顿法收敛很快,被认为非常有效。
#include <math.h>
typedef unsigned long long ULONG;
class Solution {
public:
int sqrt_newton(int x) {
if (x==0) return x;
double dividend = x;
double val = x;
double last;
do {
last = val;
val = (val + dividend / val) * 0.5;
} while(abs(val - last) > 1e-9); // precision
return (int)val;
}
}
我们
用最后一次检查当前迭代
,如果差异小于例如1e-9 (EPSILON),那么我们认为我们已经获得了所需的精度。
另一个类似的问题是检查给定的整数是否是有效的完美平方。