https://blog.csdn.net/2601_95366422/article/details/158690947
上节课连接
一.题目
二.思路讲解
对于求一个非负整数 x 的算术平方根(要求返回整数部分),我们首先需要判断这个问题是否具备二分性 。所谓二分性,就是我们可以通过某种规则将搜索区间一分为二,并且能够确定目标值在哪一半。这里,算术平方根的值一定落在区间 [0, x] 内,而且这个区间是单调递增 的------随着数值增大,其平方也增大。正是这种单调性 赋予了问题二分性 ,因此我们可以采用二分查找来逼近答案。
接下来要确定使用左端点 还是右端点 模板。我们需要找到的是满足 mid * mid <= x 的最大 mid 值,也就是最后一个满足条件的数。左端点模板用于寻找第一个满足某个条件 的位置,而右端点模板则用于寻找最后一个满足条件 的位置。显然,本题的需求是最后一个小于等于目标 的值,因此应该采用右端点二分查找。使用右端点模板,我们可以直接定位到满足条件的最大值,即算术平方根的整数部分。
理解这一点很关键:左边界 找的是第一个,右边界找的是最后一个。根据问题是要找最大还是最小,选择合适的模板即可。
三.代码演示
cpp
class Solution {
public:
int mySqrt(int x)
{
long left = 0;
long right = x;
while(left < right)
{
long mid = left + (right - left + 1)/2;//求右端点
long long sum = mid * mid;
if(sum > x)
right = mid - 1;
else
left = mid;
}
return left;
}
};
四.代码讲解
一、循环条件与中点计算
采用 while (left < right) 作为循环条件,而不是 left <= right。这是因为当左右指针相遇时,区间内只剩一个元素,此时无需继续二分 ,可以直接在循环外返回该元素。这种写法可以避免死循环,并符合右端点模板的常规写法。
中点计算采用向上取整 的公式,即 mid = left + (right - left + 1) / 2。当区间长度为偶数时,中点会偏向右边 ,这确保了当 left 和 right 相邻时,中点指向 right,从而保证区间能够正确收缩,不会出现 left 原地不动的情况。向上取整 是右端点查找的精髓,也是避免死循环的关键。
二、区间缩进规则
在循环中,根据 mid * mid 与 x 的比较结果,按照以下规则更新指针:
-
当
mid * mid > x时 :说明 mid 的平方已经超过了 x,那么所有大于等于 mid 的数都不可能满足条件,因此最后一个满足条件的数一定在 mid 的左侧 。所以将右指针移动到 mid - 1,即舍弃右半区间。 -
当
mid * mid <= x时 :说明 mid 本身是满足条件的,但右边可能还有更大的数也满足条件。因此将左指针移动到 mid,保留 mid 在搜索区间内 ,继续向右寻找更大的可行解。这里注意不能写成left = mid + 1,因为 mid 可能是最终答案,必须保留。
三、循环结束后的处理
循环结束时,left 与 right 相等,此时指向的位置就是最后一个满足 mid * mid <= x 的数 ,即 x 的算术平方根的整数部分。直接返回 left 即可,无需额外判断,因为题目保证 x 是非负整数,且当 x = 0 时循环也能正确处理(区间只有 0)。
四、为什么不需要分类讨论
与之前"搜索插入位置"的右端点解法不同,本题循环结束后无需分类讨论,原因在于:我们寻找的是满足条件的最大值 ,而循环结束时 left 一定落在可行解的范围内。即使 x = 0,初始 left = 0, right = 0,循环不执行,直接返回 0,正确。当 x 为其他值时,由于我们始终在可行解区间内收缩,最终 left 必然指向满足条件的最大值。
五、与左端点模板的对比
如果错误地采用左端点模板(向下取中,缩进规则为 if (mid * mid < x) left = mid + 1; else right = mid),循环结束后 left 会指向第一个满足 mid * mid >= x 的位置 ,这可能是平方刚好等于 x 或第一个超过 x 的数,需要根据情况调整(**比如平方大于 x 则返回 left - 1,或者找到最左边的但是右边的也可以满足条件)。**虽然也能求解,但逻辑更绕,且容易出错。而右端点模板直接命中目标,更加直观。
cpp
class Solution {
public:
int mySqrt(int x)
{
// 使用二分查找在 [0, x] 范围内寻找平方根的整数部分
long left = 0;
long right = x;
// 标准二分查找循环
while(left < right)
{
// 计算中点,防止直接 (left + right) / 2 可能溢出
long mid = left + (right - left) / 2;
// 计算中点的平方,使用 long long 防止乘积溢出
long long sum = mid * mid;
// 如果平方小于 x,说明 mid 可能是解,但解肯定在 [mid+1, right] 之间
if(sum < x)
{
left = mid + 1;
}
// 如果平方大于等于 x,说明解在 [left, mid] 之间(包含 mid)
else
{
right = mid;
}
}
// 需要判断 left 是否过小,如果 (left+1)^2 <= x,说明结果应该是 left+1
if((left+1) * (left+1) <= x)
return left + 1;
// 如果 left^2 > x(太大了),说明结果应该是 left-1
else if (left * left > x)
return left - 1;
// 否则 left 就是正确结果
return left;
}
};
六、细节注意
-
使用
long类型定义 left、right 和 mid,是因为 x 最大可达 2^31 - 1,mid * mid 可能超出 int 范围,因此用long long存储乘积避免溢出。 -
初始 right 设置为 x,对于 x = 0 或 1 也能正确工作(0 的平方根是 0,1 的平方根是 1)。
七、总结
本题是右端点二分查找的经典应用。关键在于正确运用向上取中 和区间缩进规则,使区间最终收敛到满足条件的最大值。掌握这一解法,不仅能解决求平方根问题,也为处理其他"最大值可行解"类问题提供了通用思路。
