题目如下图示:
更多精彩文章请关注微信公众号:TanLiuYi00
题目要求非负整数 x 的平方根,相当于求函数 y = √x 中 y 的值。
函数 y = √x 图像如下:
从上图中,可以看出函数是单调递增的,满足二分查找的条件(区间是有序的),所以可以考虑用二分查找去做。
解题细节
1、由于当 x 为非负整数时,其平分根一定在区间 [1, x/2 + 1] 内,所以为了缩小查找的范围,二分的左右边界分别取 left = 1 和 right = x / 2 + 1,而不取 0 和 x;
2、为了防止在查找过程中比较 mid * mid 和 x 中,前者值太大,超出整型范围而溢出,取 mid 与 x / mid 进行比较(mid 为 1 中 的 left 和 right 的和的平均值,故不会等于 0)。
Show me the Code
// C++ 版本
class Solution {
public:
int mySqrt(int x) {
int left = 1, right = (x >> 1) + 1;
while (left <= right) {
/* 防止溢出 */
int mid = left + ((right - left) >> 1);
/* mid 大于 √x ,在 mid 前半区间查找 */
if (mid > x / mid) {
right = mid - 1;
/* mid 小于 √x ,在 mid 后半区间查找 */
} else if (mid < x / mid) {
left = mid + 1;
/* mid 等于 √x ,查找到直接返回 */
} else {
return mid;
}
}
return right;
}
};
# golang 版本
func mySqrt(x int) int {
l, h:= 1, x/2 + 1
for l <= h {
mid := l + (h - l) / 2
if mid == x / mid {
return mid
} else if mid > x / mid {
h = mid - 1
} else {
l = mid + 1
}
}
return h
}
说明
-
right = x / 2 + 1,而不是 right = x / 2,因为当 x = 1 时,如果 right 取 x / 2 的话,由于 x/2 = 0,此时 left = 1 大于 right,直接循环跳出,导致 x 的平方根为 0 而不是 1 出错;
-
最后是 return right 还是 return left,可以通过调试得出,循环退出的条件是 left = right + 1,当 x = 8 的时候,题目要求返回的是 2 ,如果最后是 return left 的话,返回的结果是 3 ,所以返回的是 right 。
进一步补充
可能有些童鞋会问到,你上面写的循环的条件为何是 left <= right 而不是 left < right,其实这两个条件都可以,主要区别在于:
循环结束的条件不一样,前者是 left = right + 1 后者是 left = right。
定义的查找区间的右边界取值不一样,前者右边界取值为 x / 2 + 1,后者为 x / 2 + 2。
这里会提到一个循环不变量的概念:
循环不变量
1. 初始化:它在循环的第一轮迭代开始之前,应该是正确的。
2. 如果在循环的某一次迭代开始之前它是正确的,那么,在下一次迭代开始之前,它也应该保持正确。
3. 当循环结束时,不变式给了我们一个有用的性质,它有助于表明算法是正确的。
所以本题循环的条件可以是 left <= right 也可以是 left < right,关键是需要一直维护对应的区间 [left, right] 或 [left, right)。下面补充一个循环条件为 left < right 的 C 语言代码,最后返回的是 right - 1,是因为:
1. 定义的查找区间是左闭右开的,即 [left, right),取不到右边界 right;当 left == right 时,循环退出,但是 right 取不到,所以取 right - 1;
2. 通过调试也能得出。
Show me the Code
int mySqrt(int x){
int left = 1, right = x / 2 + 2;
// 循环不变量 始终维持在区间 [left, right) 中查找,当 left = right 时,区间为空,查找结束
while (left < right) {
// 防止溢出
int mid = left + ((right - left) >> 1);
// mid 大于 √x ,在 mid 前半区间 [left, mid) 中查找
if (mid > x / mid) {
right = mid;
// mid 小于 √x ,在 mid 后半区间 [mid + 1, right) 中查找
} else if (mid < x / mid) {
left = mid + 1;
// mid 等于 √x ,代表查找到 target,则直接返回
} else {
return mid;
}
}
return right - 1;
}
更多精彩内容,请关注 微信公众号: TanLiuYi00
欢迎大家提出意见或建议,谢谢!