文章目录
- 一、在排序数组中查找元素的第一个和最后一个位置
- [二、x 的平方根](#二、x 的平方根)
- 三、寻找峰值
一、在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
解题思路
- 进阶的二分算法题,我们要找到一个子数组而不是单个数据,我们可以利用数组的"二段性"来使用二分查找算法分别查找目标子数组的左、右边界。
代码实现及解析
java
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] ret={-1,-1};
if(nums.length==0) return ret;//处理空数组
//查找左边界
int left=0,right=nums.length-1;
while(left<right){//取等就死循环,相遇后本来也就得到结果了,还接着判断,求出的mid永远都是相遇点,right边界判断后也一直不会移动
int mid=left+(right-left)/2;//由于下面的执行:right=mid,所以必须选择"取左边"的mid求法
if(nums[mid]<target){//判断mid的位置来决定边界的收缩
left=mid+1;
}else{
right=mid;
}
}
//循环结束,left与right已相遇,看看相遇位置是否为target,若不是,则说明数组中就没有目标值
if(nums[left]==target){
ret[0]=left;
}else{
return ret;
}
//同样来查找右边界
right=nums.length-1;//left就不用再从头开始了
while(left<right){
int mid=left+(right-left+1)/2;//由于下面的执行:left=mid,所以必须选择"取右边"的mid求法
if(nums[mid]>target){
right=mid-1;
}else{
left=mid;
}
}
//循环结束,left与right已相遇
ret[1]=right;
return ret;
}
}
总结
二段性:
的确,二分算法在有序数组中具有很大的优势,但是在非排序的数据场景下也是可以使用的,本题的代表性还没有那么强,但也可以体现出这种性质当一组数据可以依据某个/段数据而分为两个区间,我们可以把这个性质称为"二段性",比如:[0,分界点-1]、[分界点,arr.length-1] 这样的两个区间,此时就可以使用二分算法来解题。我们查找过程中的 mid 无非就在这两个区间里,依据 mid 在哪个区间来决定收缩左区间或右区间到 mid 位置,来靠近分界点
以上就是观察数据的二段性来应用二分算法的介绍
使用二分算法很容易出现代码死循环的情况,这就要求在书写代码时多注意细节问题死循环多出现在循环接近结束时遇到了临界情况,由于 while 循环或者 left、right 指针移动没有处理好临界情况而造成了死循环,可以着重思考如何处理临界情况来处理死循环问题。而由临界情况造成的直接死循环原因常见于left、right指针一直在原地移动,无法正常查找
求中点 mid 的两种方式:
1. mid=left+(right-left)/2
2. mid=left+(right-left+1)/2
不直接使用 ( right + left ) / 2 是为了防止加和而导致的整数溢出
第一种方式在偶数情况下 mid 会取到中间两个数据的前者,第二种方式在偶数情况下 mid 会取到中间两个数据的后者,那有什么区别呢?正确选择这两种求 mid 的方式才可以避免死循环,若right执行的移动逻辑是:right=mid ,当left和right已经相邻时,若采用第二种计算方式,mid 的计算结果则为 right 的位置,那么很明显 right 就会一直在原地移动造成死循环,所以这个时候 mid 的计算就要采取第一种方式。当left执行的移动逻辑是:left=mid 时,同理,mid 的计算方式就要采取第二种方式
利用二分算法求非递减数组中重复数据的左、右边界的这个算法的代码框架常用,可以熟悉下其代码框架:
查找左边界:
java
while(left <right){//这里取等就死循环,相遇后本来也就得到结果了,还接着判断,求出的mid永远都是相遇点,right边界判断后(right=mid)也一直不会移动
int mid = left + (right-left)/2;//这里使用哪个 mid 计算式可以先不思考,看下面left、right指针的移动策略来决定
if(.....)
left=mid + 1;//"+1"使left指针最终得以跳出其所在的非法区域:(目标值,arr.length-1],并停在目标值位置,与right会和
else
right = mid;//right不执行"+1"是为了保证其不会越处其所在的合法区域:[0,目标值],最终在目标值位置与left会和
}
//这种策略就是要left、right两指针在目标值处相遇得到结果
查找右边界:
java
while(left<right){
int mid = left + (right - left + 1)/2;
if(...)
left = mid;
else
right=mid-1;
}
二、x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
0 <= x <= 231 - 1
解题思路
- 遍历1~x之间的值,以目标值为分界点,将数据分为"平方<=x的区域"和"平方>x的区域"来使用二分算法
代码实现及解析
java
class Solution {
public int mySqrt(int x) {
if(x<1) return 0;
long left=1,right=x;//用long防止整数溢出
while(left<right){
long mid=left+(right-left+1)/2;
if(mid*mid>x){//在非法区域,right最后要跳出非法区域
right=mid-1;
}else{
left=mid;//left一直在合法区域内移动
}
}
return (int)right;
}
}
总结
对数据的"二段性"的利用的一个很好的实例,将数据分为"平方<=x的区域"和"平方>x的区域"来使用二分算法可以对照上面的代码框架与本题的代码进行对比,提高对该代码框架使用的熟练度
扩展:
- "山峰数组"
"山峰数组"指数据先递增再递减的数组,可以发现它的二段性是:前一段数据在递增区间(nums[mid]<nums[mid+1]),后一段数据在递减区间(nums[mid]>nums[mid+1])
- "旋转数组"
比如对于一个排序数组,且无重复数据。原数组 nums = [0,1,2,4,5,6,7],若向右旋转 4 次,则可以得到 [4,5,6,7,0,1,2],找出旋转数组中的最小值。这种数组的二段性则为 nums[mid]<=nums[n-1] 和 nums[mid]>nums[n-1] (n=nums.length)这两段
可以发现找出数据二段性的关键是要找到一个参照物来划分出数据的不同区域要打开思维多进行尝试,我们也可以利用数值与下标的关系来划分数组:区块1与其下标有着A关系(val==Index),区块2与其下标有着B关系(val>Index)
三、寻找峰值
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
提示:
1 <= nums.length <= 1000
-231 <= nums[i] <= 231 - 1
对于所有有效的 i 都有 nums[i] != nums[i + 1]
示例 :
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
解题思路
- 本题关键在于依题意找到数据的"二段性",之后就可以按照熟知的二分算法框架来解题
代码实现及解析
java
class Solution {
public int findPeakElement(int[] nums) {
//二分查找框架:
int left=0,right=nums.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>nums[mid+1]){//此时在[left,mid]这个区间一定存在一个峰值
right=mid;
}else{//此时在[mid+1,right]这个区间一定存在一个峰值
left=mid+1;
}
}
//相遇,得到了目标值
return left;
}
}
总结
本题是体现数据二段性的非常具有代表性的一题,让我们看到即使完全乱序的数据也是可以且非常适合使用二分算法的,只要我们能细心发现数据的二段性,然后"让mid去探一探目标值应该在那个方向",在使边界收缩,便可以靠近目标值当我们得到mid后,取两个数据:nums[mid]、nums[mid+1],有一下两种情况(题目已说明无重复元素):
- nums[mid]>nums[mid+1]
- 也就是说 mid~mid+1 是呈递减趋势的,若mid往前也是呈递减趋势的那mid就是峰值;若mid往前是呈递增趋势的,那么在左边这个区域只要出现递减那么那个位置就是峰值,即使一直呈递增趋势,由于题目已假设nums[-1] = nums[n] = -∞,所以此时nums[0]就是峰值。
- 综上所述,在[left,mid]这个区间一定存在一个峰值,那么right边界就可以往mid这里收缩了
- nums[mid]<nums[mid+1]
- 同样可证在[mid+1,right]这个区间一定存在一个峰值,那么left边界就可以往mid+1这里收缩了
- 如此一来就可以使用二分算法来解题了,让边界一直按以上逻辑收缩,直到相遇得峰值。
- 所以在使用二分算法时还是要仔细思考数据的规律,分析出数据的二段性。
分析出数据的二段性之后,还是要捋清这种二分算法的思路:通过 mid 所在的区域来判断左、右边界的移动,使边界一次又一次地靠近目标值,最后才在边界相遇时得到了目标值,而由于while(left<right)循环的判断条件,使得当循环结束时left和right恰好相遇。mid 是由left和right计算而得,而left和right又是依靠 mid 实现移动的,所以不需要担心left会突然跳过right