给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10]
, target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10]
, target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0 输出:[-1,-1]
方法一:直接搜索
public class Solution {
public int[] SearchRange(int[] nums, int target) {
int searchIndex=nums.Length;
int start=-1;
int end=-1;
for(int i=0;i<searchIndex;i++)
{
if(nums[i]!=target) continue;
else if(start==-1) start=end=i;
else {
end=i;
}
}
return [start,end];
}
}
直球思路,用时52%,内存5%
猜测由于每次都需要完整遍历导致效率不佳
稍微修改,在寻找目标数据后单独进行搜索
public class Solution {
public int[] SearchRange(int[] nums, int target) {
int searchIndex=nums.Length;
int start=-1;
int end=-1;
int i=0;
for(i=0;i<searchIndex;i++)
{
if(nums[i]==target) {
start=end=i;
break;
}
}
while(++i<searchIndex&&nums[i]==target)
{
end=i;
}
return [start,end];
}
}
时间52%,内存14%
方法二:二分法
先通过二分法找到target,再向左向右进行寻找
public class Solution {
public int[] SearchRange(int[] nums, int target) {
int searchIndex=nums.Length;
int start=-1;
int end=-1;
int left=0;
int right=searchIndex-1;
int middle=-1;
while(right>=left)
{
middle=left+(right-left)/2;
if(nums[middle]==target) break;
else if(nums[middle]<target) left=middle+1;
else right=middle-1;
}
if(middle!=-1&&nums[middle]==target)
{
int temp=start=end=middle;
while(--middle>-1&&nums[middle]==target) start=middle;
while(++temp<searchIndex&&nums[temp]==target) end=temp;
}
return [start,end];
}
}
时间52% 内存59%
方法三:最优二分法--在搜索的同时直接搜索出区间
对于二分法的理解:二分法主要区分在于对判断条件的理解,第一while区间主要用于循环的判断,即完成对所有可能的遍历,第二个mid与target的判断主要用于是判断target还是判断极限点,
理解:二分法类似于极限逼近,if的两个判断类即两端的极点。当nums[mid]>=target即右端为大于等于target的点,左端为小于的点,而当nums[mid]>target即右端为大于target的点,右端为小于等于的点。,对于mid的取值分为两种即是否加一,这一步骤决定了最后的值是偏左还是偏右
区间为[low,high]的代码
public class Solution {
public int[] SearchRange(int[] nums, int target) {
int start =SearchStart(nums,target);
if(start<0){
return new int[] {-1,-1};
}
int end =SearchEnd(nums,target);
return new int[] {start,end};
}
public int SearchStart(int[] nums, int target) {
int low=0,high=nums.Length-1;
while(low<=high)
{
int mid=low+(high-low)/2;
if(nums[mid]>=target)
{
high=mid-1;
}
else low=mid+1;
}
return low<nums.Length&&nums[low]==target?low:-1;
}
public int SearchEnd(int[] nums, int target) {
int low=0,high=nums.Length-1;
while(low<=high)
{
int mid=low+(high-low+1)/2;
if(nums[mid]<=target)
{
low=mid+1;
}
else high=mid-1;
}
return high<nums.Length&&nums[high]==target?high:-1;
}
}
区间分别为左开右闭,和左闭右开的代码-leetcode标准答案
public class Solution {
public int[] SearchRange(int[] nums, int target) {
int start = SearchStart(nums, target);
if (start < 0) {
return new int[]{-1, -1};
}
int end = SearchEnd(nums, target);
return new int[]{start, end};
}
public int SearchStart(int[] nums, int target) {
int low = 0, high = nums.Length - 1;
while (low < high) {
int mid = low + (high - low) / 2;
if (nums[mid] >= target) {
high = mid;
} else {
low = mid + 1;
}
}
return low < nums.Length && nums[low] == target ? low : -1;
}
public int SearchEnd(int[] nums, int target) {
int low = 0, high = nums.Length - 1;
while (low < high) {
int mid = low + (high - low + 1) / 2;
if (nums[mid] <= target) {
low = mid;
} else {
high = mid - 1;
}
}
return low < nums.Length && nums[low] == target ? low : -1;
}
}
两者均可运行且时间极短
总结
对于二分法来说,有非常多需要注意的点
while(low<=high) //行1
{
int mid=low+(high-low)/2; //行2
if(nums[mid]>=target) //行3
{
high=mid-1; //行4
}
else low=mid+1; //行5
}
这是一个标准的二分法代码,其中很多地方的切换会导致很多不一样的效果
行1
描述的是循环的能运行的区间的条件,决定了是否将所有可能值全循环一遍
行二
描述的是mid的取值,其另一种写法为:int mid=low+(high-low+1)/2; 其是否加一,决定了在low和high其中为一奇一偶时,其mid取值偏向于左边还是右边。当加一偏向右,不加偏向左
行三
描述的是 nums[mid] 与 target 的大小对比。我们可以假设此处的if条件为极限逼近,其条件即为逼近两点的极限值。有四种可能,且配合mid的取值,决定获得的是左侧的极限值还是右边。
拿上图举个例子,在此时,为 nums[mid]>=target 代表了其极点:右为target,左为小于target的值,而mid为偏向左,即获得便是小于target的最大值,但是由于最后再次进行了一次low=mid+1;所以获得值为target最小值,当然如果你选择先加减,再求mid那么最后就不需要加一,下列代码也是求得target最小值
while(low<=high)
{
if(nums[mid]>=target)
{
high=mid-1;
}
else low=mid+1;
mid=low+(high-low+1)/2;
}
行三行四
分别对应着不断缩小的距离,直观决定了某个值无法重复使用