*X的平方根问题:
给你一个非负整数x,计算并返回x的算术平方根。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x 乘乘 0.5 。 *
class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid+1;
} else {
r = mid-1;
}
}
return ans;
}
}
*数的三次方根问题:
给定一个浮点数 n,求它的三次方根。
输入格式
共一行,包含一个浮点数n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留6位小数。*
class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid+1;
} else {
r = mid-1;
}
}
return ans;
}
}
在处理浮点数的二分查找问题时,关键在于理解如何根据当前的中间值 mid
来调整搜索区间。对于立方根的问题,我们的目标是找到一个数 mid
,使得 mid^3
尽可能接近给定的数 n
。如果 mid^3
小于 n
,我们知道答案一定在 mid
的右侧,因此我们将 l
设置为 mid
。如果 mid^3
大于 n
,我们知道答案在 mid
的左侧,因此我们将 r
设置为 mid
。
对于平方根的问题,我们的目标是找到一个数 mid
,使得 mid^2
尽可能接近给定的数 x
。由于我们只需要整数部分的结果,我们可以通过检查 mid^2
是否小于或等于 x
来决定是向左还是向右移动。如果 mid^2
小于或等于 x
,我们知道 mid
是一个可能的答案,并且我们可以尝试一个更大的数,因此我们将 l
设置为 mid+1
。如果 mid^2
大于 x
,我们知道 mid
太大了,因此我们将 r
设置为 mid-1
。
在平方根的代码中,我们使用 (long) mid * mid
来避免整数溢出,因为 mid
的平方可能会超出 int
类型的范围。同时,我们使用 ans
变量来记录当前找到的最大可能答案,因为我们知道答案一定是一个整数,所以我们在每次迭代中更新 ans
。
在立方根的问题中,我们不需要担心整数溢出的问题,因为我们处理的是浮点数。我们的目标是找到一个浮点数,使得它的立方尽可能接近给定的数 n
,并且我们保留六位小数。因此,我们在每次迭代中更新 ans
,并在循环结束后输出 l
,因为 l
和 r
会收敛到一个非常接近真实答案的值,而我们需要的是更小的边界值。
两种方法的主要区别在于如何处理中间值 mid
以及如何根据 mid
调整搜索区间。在平方根问题中,我们更关心整数部分,而在立方根问题中,我们关心的是浮点数的精确度。
具体什么时候采取l = mid什么时候采取l = mid + 1
在二分查找算法中,选择 l = mid 还是 l = mid + 1 取决于你正在解决的问题的性质,以及你如何定义“更接近”这个概念。
对于立方根问题:
你想要找到一个数 mid,使得 mid^3 尽可能接近给定的数 n。在这种情况下,如果 mid^3 小于 n,那么 mid 太小了,但 mid 本身可能是最终答案的一个好候选,因为随着 mid 的增加,mid^3 也会增加,可能会超过 n。因此,你保留 mid 作为可能的答案,并尝试增加 l 来探索更大的值。所以,你使用 l = mid。
对于平方根问题:
你想要找到一个整数 mid,使得 mid^2 尽可能接近给定的数 x,但不超过它。因为你只关心整数部分,所以如果 mid^2 小于或等于 x,mid 可能是答案,但 mid + 1 肯定不是答案,因为 (mid + 1)^2 会大于 x。在这种情况下,你不需要检查 mid + 1,因为它不可能是答案。因此,你更新 l = mid + 1 来避免不必要的检查。
总结来说:
当你关心的是找到一个数,使得它的某个幂次尽可能接近给定的数时(如立方根问题),如果中间值的幂次小于给定的数,你可能会保留中间值作为可能的答案,并尝试增加 l 来探索更大的值(l = mid)。
当你关心的是找到一个数的整数部分,使得它的某个幂次不超过给定的数时(如平方根问题),如果中间值的幂次小于或等于给定的数,你知道下一个整数的幂次肯定会超过给定的数,因此你可以直接更新 l = mid + 1 来避免不必要的检查。
当然可以。使用 l = mid + 1
更有效的情况通常出现在你需要找到一个边界值,并且这个边界值是使得某个条件首次不满足的点。这种情况下,二分查找的目标是找到某个特定属性的上限或下限。以下是几个例子:
1. 寻找数组中的最大值
假设你有一个已排序的数组,你想找到数组中最大值的索引。你可以使用二分查找来确定这个最大值的位置。
int l = 0, r = array.length - 1, maxIndex = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (array[mid] > array[mid + 1]) {
maxIndex = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
在这个例子中,如果 array[mid]
大于 array[mid + 1]
,说明 mid
是一个局部最大值的索引,但我们要找的是整个数组的最大值,所以我们向左移动 r
来继续寻找。如果 array[mid]
不大于 array[mid + 1]
,说明 mid
还不是最大值,所以我们更新 l = mid + 1
来探索更大的索引。
2. 寻找满足条件的最小整数
假设你想找到最小的整数 x
,使得 x^2
大于或等于给定的数 n
。这是一个典型的使用 l = mid + 1
的情况。
int l = 0, r = n, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (mid * mid < n) {
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
在这个例子中,如果 mid * mid
小于 n
,说明 mid
太小了,我们需要一个更大的数,所以我们更新 l = mid + 1
。如果 mid * mid
大于或等于 n
,说明 mid
是满足条件的最小整数,所以我们更新 ans
并向右移动 r
来尝试找到更小的满足条件的整数。
3. 寻找满足特定条件的元素
假设你有一个函数 boolean condition(int x)
,你想找到最小的整数 x
,使得 condition(x)
返回 true
。
int l = 0, r = someUpperBound, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (!condition(mid)) {
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
在这个例子中,如果 condition(mid)
返回 false
,说明 mid
太小了,我们需要一个更大的数,所以我们更新 l = mid + 1
。如果 condition(mid)
返回 true
,说明 mid
是满足条件的最小整数,所以我们更新 ans
并向右移动 r
来尝试找到更小的满足条件的整数。
在所有这些例子中,使用 l = mid + 1
是因为我们在寻找一个边界值,这个边界值是使得某个条件首次满足或不满足的点。通过更新 l = mid + 1
,我们可以确保不会错过任何可能的边界值,并且可以有效地缩小搜索范围。