目录
[x 的平方根](#x 的平方根)
算法示例
前言
我们在做算法题时,遇到在数组中找特定元素时,通常会使用暴力解法来遍历数组中的元素,时间复杂度通常为O(n),但有时有些题目要求我们的时间复杂度要达到O(logN),那么我们就得使用二分查找来解决问题。
什么是二分查找?
二分查找是一种在有序数组中查找特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且同样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
但二分查找只能在有序的数组中使用吗?
其实只要有二段性,就能够使用二分查找。
二段性
二段性指的是在查找过程中,数据集可以被分成两个部分,其中一部分满足某个条件,而另一部分不满足这个条件。换句话说,就是数据集可以被划分为两个子集,其中一个子集的所有元素都具有某种性质,而另一个子集的所有元素都不具有这种性质。
接下来就通过题目来讲解。
二分查找
算法分析
本道题是要在一个升序的数组中找到一个目标值,我们首先能想到的就是使用暴力解法,遍历数组,判断目标值是否在数组中,这样的解法时间复杂度为O(n),但我们可以进行优化,使用二分查找,能让时间复杂度达到O(logn)。
算法步骤
- 初始化:定义两个指针left和right,left初始化为0,right初始化为nums.length-1,即数组的末尾元素。
- 与中间值比较 :定义mid,mid在循环中每次都是left+(right-left)/2(为了防溢出) ,判断nums[mid]和target的大小。循环条件为:left<=right
1.若nums[mid]小于target,则让left=mid+1,
2.若nums[mid]大于target,则让right=mid-1,
3.当nums[mid]==target,说明此时已经找到了目标值在数组中的位置,返回mid即可。
3.循环结束:当left和right错开时,说明此时已经查找完数组,没有找到目标值,返回-1即可。
算法代码
java
/**
* 在排序数组中查找目标值的索引。
* 该方法使用二分查找算法,在一个已排序的数组中查找目标值的索引。如果目标值存在于数组中,则返回其索引;
* 如果不存在,则返回-1。二分查找算法通过不断缩小搜索范围来提高查找效率。
*
* @param nums 一个已排序的整数数组。
* @param target 要查找的目标整数。
* @return 目标值在数组中的索引,如果不存在则返回-1。
*/
public int search(int[] nums, int target) {
/* 定义搜索范围的左右边界 */
int left = 0, right = nums.length - 1;
/* 当左边界不大于右边界时,进行循环搜索 */
while (left <= right) {
/* 计算中间位置,避免整数溢出 */
int mid = left + (right - left) / 2;
/* 如果中间位置的值小于目标值,则目标值在右半部分,更新左边界 */
if (nums[mid] < target) {
left = mid + 1;
}
/* 如果中间位置的值大于目标值,则目标值在左半部分,更新右边界 */
else if (nums[mid] > target) {
right = mid - 1;
}
/* 如果中间位置的值等于目标值,则找到目标值,返回其索引 */
else {
return mid;
}
}
/* 如果循环结束还未找到目标值,返回-1表示未找到 */
return -1;
}
时间复杂度为O(logn),n为数组长度,整个过程中,每次都是排除一半的长度.
空间复杂度为O(1),整个过程中只用到了几个变量。
算法示例
以nums= [-1,0,3,5,9,12],
target
= 9为例
第一步:初始化双指针,让left指向数组起始位置,right指向数组末尾。
第二步:与中间值进行比较,让mid=left+(right-left)/2;
我们可以看图中,此时mid下表为2,nums[mid]=3<9,让left=mid+1;
此时left=3,mid=3+(5-3)/2=4,此时恰好nums[mid]=target=9,返回mid即可。
模板
上述中这种解法就是朴素的二分查找,我们可以总结出一个模板
java
int left=0,right=数组名.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(条件) {
//处理
left=mid+1;
}else if(条件){
//处理
right=mid-1;
}else{
//处理
return mid;
}
}
在排序数组中查找元素的第一个和最后一个位置
算法分析
题意说明了要在一个排序数组中找目标值的起始位置和末尾位置,对于这道题,如果我们采用暴力枚举的话,时间复杂度能达到O(n),但题目要求我们要实现复杂度为O(logn)的算法,对于这种在有序数组中找点的题目,我们可以用二分查找。
在讲算法步骤之前,我们先来了解一下找中点的问题。
我们知道使用mid=left+(right-left)/2,能够找到中点位置,但在偶数个数的情况下,若使用这条式子,则是向下取整,找的是靠左的中点。
但如果我们想要找的是靠右的中点,那么我们可以在(right-left)之中+1,表示向上取整。
即mid=left+(right-left+1)/2
总结:
在偶数个的情况下:
- 找左中点:mid=left+(right-left)/2
- 找右中点:mid=left+(right-left+1)/2
但在奇数个的情况下,使用哪个都行。
算法步骤
- 初始化: 设置left和right,left初始化为0,right初始化为数组的末尾位置。并创建一个ans数组(长度为2)初始化为{-1,-1}。
- 排除数组为空的情况:判断数组长度是否0,若为0,则返回ans数组。
- 找左端点 :为了找到目标值最左侧的位置,当我们nums[mid]==target时,此时可能不是目标值的最左侧位置。因此,在nums[mid]>=target的时候,让right=mid ,为什么不能让right=mid-1,这是为了防止跳过目标值,从而找不到最左侧的位置。当nums[mid]<target时,说明左侧期间[left,mid]中都小于目标值,让left=mid+1 。循环条件为left<right .(此处不能取等,在left和right相遇的时候,说明此时已经找打了目标位置,或者是已经走完数组,没有找到目标值,所以当循环结束之后,我们需要判断right位置的元素是否等于target,若等于,则让ans[0]=right,反之,则返回ans)。
- 找右端点:在查找右端点时,我们需要注意:
找中点时需要+1:mid=left+(right-left+1)/2.为了找到右侧的目标位置。
当nums[mid]<=target的时候,left=mid,为什么不能让left=mid+1,这是防止跳过目标值。当nums[mid]>target时,让right=mid-1.
算法代码
java
/**
* 在排序数组中查找给定目标值的起始和结束位置。
*
* @param nums 一个按升序排列的整数数组。
* @param target 需要查找的目标值。
* @return 一个包含起始和结束位置的整数数组。如果目标值不存在,则起始和结束位置都设置为-1。
*/
public int[] searchRange(int[] nums, int target) {
// 初始化结果数组,用于存储目标值的起始和结束位置,初始值设为-1。
int[] ans=new int[2];
ans[0]=ans[1]=-1;
// 初始化左右指针。
int left=0,right=nums.length-1;
// 如果数组为空,则直接返回结果数组。
if(nums.length==0) return ans;
// 使用二分查找法寻找目标值的起始位置。
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>=target) right=mid;
else left=mid+1;
}
// 如果右指针指向的值不等于目标值,则目标值不存在,直接返回结果数组。
if(nums[right]!=target) return ans;
else ans[0]=right; // 否则,更新起始位置。
// 重新初始化左右指针,寻找目标值的结束位置。
// 找右端点
left=0;right=nums.length-1;
// 使用二分查找法寻找目标值的结束位置。
while(left<right){
int mid=left+(right-left+1)/2;
if(nums[mid]<=target) left=mid;
else right=mid-1;
}
ans[1]=left; // 更新结束位置。
return ans; // 返回结果数组。
}
时间复杂度为O(n),n为数组长度。
空间复杂度为O(1),只用了常数个变量。
算法示例
以nums = [5,7,7,8,8,10], target = 8为例
第一步:初始化
left=0,right=5,ans[2]={-1,-1}
第二步:找左端点
- left=0,right=6,mid=left+(right-left)/2=0+(5-0)/2=2,nums[mid]=7<target=8.让left=mid+1=3
2.此时mid=3+(5-3)/2=4,nums[mid]=target=8,让right=mid=4
3.mid=3+(4-3)/2=3,nums[mid]=8=target,此时让right=mid=3,同时left==right,结束循环。
4.此时判断right位置的值是否为目标值相等,让ans[0]=right=3.
第三步:找右端点
- 由于在找左端点时,right此时已经走到了左端点的位置,但此时我们需要让right=nums.length-1,left=0(此处left可以不为0,从当前位置直接开始,这里为了方便观看,从0开始)
- mid=0+(5-0+1)/2=3,此时nums[mid]==target,让left=mid。
3.mid=3+(5-3+1)/2=4,nums[mid]=4=target,让left=mid。
4.mid=4+(5-4+1)/2=5,nums[mid]>target=8,此时让right=mid-1.
5.此时left==right,结束循环,ans[1]=4
第四步:返回结果
此时ans={3,4},返回ans。
搜索插入位置
算法分析
本道题是在排序数组中查找目标值,若找不到目标值,则返回插入位置。对于这道题,如果我们采用暴力遍历的解法,时间复杂度为O(n).但题目要求我们使用O(logn)的算法,那么我们可以采用二分查找的方法来解决。
算法步骤
- 初始化:设置left和right,left初始化为0,right为nums.length-1。
- 查找位置:设置mid,mid=left+(right-left)/2,当nums[mid]<target,让left=mid+1,当nums[mid]>=target时,让right=mid,为什么在等于的时候还要让right=mid?这是为了让target在数组中与target相等数的最左侧插入(才能满足题目要求)。循环条件为:left<right,(当left和right相遇,说明已经找到结果)
- 处理边界:当target的值比nums[left]大时,此时插入的位置为left+1。
- 返回结果:判断target和nums[left]的大小之后,若大于则返回left+1,反之,返回left。
算法代码
java
/**
* 在排序数组中查找目标值的插入位置。
* 通过二分查找法,确定目标值应该插入的位置,以保持数组的有序性。
*
* @param nums 排序数组,数组中的元素升序排列。
* @param target 目标值,我们需要找到将其插入到数组中的位置。
* @return 返回目标值应该插入的位置索引。
* 如果目标值存在于数组中,则返回目标值的索引;
* 如果目标值不存在于数组中,则返回目标值应该插入的位置索引。
*/
public int searchInsert(int[] nums, int target) {
/* 初始化左右指针 */
int left = 0, right = nums.length - 1;
/* 使用二分查找法缩小查找范围 */
while (left < right) {
/* 计算中间位置,避免整数溢出 */
int mid = left + (right - left) / 2;
/* 如果中间位置的值小于目标值,则目标值应该在中间位置的右侧 */
if (nums[mid] < target) left = mid + 1;
/* 如果中间位置的值大于等于目标值,则目标值应该在中间位置或其左侧 */
else right = mid;
}
/* 检查最终的左指针位置 */
/* 如果数组中的最后一个元素小于目标值,则目标值应该插入到数组的最后 */
if (nums[left] < target) return left + 1;
/* 如果数组中的最后一个元素大于等于目标值,则目标值已经在数组中,返回左指针位置 */
return left;
}
时间复杂度为O(logn),n为数组的长度
空间复杂度为O(1),只用了常数个的变量。
算法示例
以nums = [1,3,5,6], target = 5为例
第一步:初始化
left=0,right=3
第二步:找插入位置
- mid=left+(right-left)/2=0+(3-0)/2=1,mid=3<target=5,让left=mid+1=2
2.mid=2+(3-2)/2=2,nums[mid]=5=target=5,让right=mid。
3.此时left==right,结束循环。
第三、四步:判断target和nums[left]的大小,返回结果,此时target=nums[left],返回left=2
x 的平方根
算法分析
本道题是想查找一个整数的算法平方根,若我们使用暴力遍历的方法,时间复杂度为O(N),我们可以采用二分查找的方法来进行优化。在遍历的过程中,每次缩小一般的数据量,来减少循环次数,时间复杂度为O(logN),
算法步骤
-
初始化:设置left和right,让left=0,right=x,(这里为了后续操作防止溢出,需要设置为long类型)。
-
x值判断:如果x的值是小于1的,则直接返回0.
-
查找x的算术平方根:设置mid=left+(right-left+1)/2,为了防溢出,mid的类型也为long。当mid*mid<=x时,此时让left=mid;反之,mid*mid>x,此时让right=mid-1。
-
结束循环:当left和right相遇时,此时说明已经找到了x的算术平方根,返回结果即可。
算法代码
java
/**
* 计算一个整数的平方根的整数部分。
* 采用二分查找的方法来逼近平方根的值,避免了浮点数运算,提高了计算精度。
*
* @param x 待求平方根的非负整数
* @return 平方根的整数部分
*/
public int mySqrt(int x) {
// 如果x小于1,直接返回0,因为0和负数没有平方根
if(x < 1) return 0;
// 初始化左右边界,左边界为1,右边界为x
long left = 1, right = x;
// 使用二分查找法来逼近平方根的值
while(left < right) {
// 计算中间值,避免整数溢出,并确保mid为整数
long mid = left + (right - left + 1) / 2;
// 如果中间值的平方小于等于x,说明平方根的值在mid或mid的右侧
if(mid * mid <= x) {
left = mid;
} else {
// 否则,平方根的值在mid的左侧
right = mid - 1;
}
}
// 返回查找到的平方根的整数部分
return (int) left;
}
时间复杂度为O(logX),X为整数的值。
空间复杂度为O(1),只用了常数个变量。
算法示例
以x=4为例
第一、二步:初始化,判断x是否小于0
left=0,right=4
第三步:进行查找x的算术平方根
- mid=left+(right-left+1)/2=1+(4-1+1)/2=3,mid*mid=9>x=4,让right=mid-1
2.mid=1+(2-1+1)/2=2,mid*mid=4=x,让left=mid。此
第四步:返回结果
此时left=right,说明找到了x的算术平方根,返回left(需要注意强转为int)
二分查找上篇就先到这了~
若有不足,欢迎指正~