二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。(百度百科)
接下来我们通过一个例子来近一步了解二分查找
leetCode 35.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
输入: nums = [1,3,5,6], target = 0
输出: 0
首先我们来分析,该题如何使用到了二分查找,因为数组是单调递增的,所以,我们可以通过mid=(left+right)/2来将数组分为左半区间和右半区间,并通过nums[mid]和target的大小来确定target所属的位置在数组的左半区间还是右半区间,并不断地二分数组,直至找到正确的区间
按照这个思路,在对比nums[mid]和target的时候,有以下三种情况:
1.nums[mid] = target
这种情况下,我们找到了目标值,返回其对应的索引mid即可
2.nums[mid] > target
这种情况下,说明target位于左区间,因此我们接下来要在左区间进行查找,因此将right设为mid-1,这样在下一次二分中,我们二分的区间就是0 ~ mid-1即左区间
3.nums[mid] < target
这种情况下,说明target位于右区间,因此我们接下来要在右区间进行查找,因此将left设为mid+1,这样在下一次二分中,我们二分的区间就是mid+1 ~ nums.length-1即右区间
最后一个问题,我们什么时候停止二分呢?这个终止条件其实要看情况进行分析。就本题而言,退出二分的条件应该是left>right,因为左边界超过右边界时,对于这样的数组已经没有意义,也没法二分了。
按照这个思路,代码如下:
public int searchInsert(int[] nums, int target) {
//数组为空时,target必然不存在
if(nums==null || nums.length==0){
return -1;
}
int left=0,right=nums.length-1;
while(left<=right){
int mid= left+((right-left)>>1);
//找到target
if(nums[mid]==target){
return mid;
}
//target位于左半区间
else if(nums[mid]>target){
right=mid-1;
}
//target位于右半区间
else if(nums[mid]<target){
left=mid+1;
}
}
return left;
}
还需要额外说明的是,如果原数组中没有目标元素,退出循环前的最后一步,必然有left=right,但target未知,执行的操作可能是left=mid+1也可能是right=mid-1,但无论如何返回的一定是较大的那个。因为对于nums[mid] < target这种情况,target>nums[i] 所以此时,应返回较大的Left,表示插入位置应位于nums[i]后面,对于nums[mid] > target,target<nums[i] 此时right-1,left较大,应返回left,表明插入位置应位于nums[i]处。这就是最后返回的是left和right里面的最大值的原因。
接下来在此基础上,我们再对二分查找做近一步地拓展。
leetCode34.查找元素第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
本题的大致思路与35题类似,但是有一个地方不一样,那就是数组虽然是有序的,但元素可以重复出现,也就是说,当target = nums[mid]时,这个时候找到的mid并不一定就是第一个元素或者最后一个元素。
既然如此,我们需要对这种情况进行处理,对于查找第一个出现的元素的索引的情况来说,我们可以在target = nums[mid]时,将right 设为 mid继续进行二分查找。这样缩小了查找区间,又不会丢掉我们要查找的目标值。同时,应将循环条件改为left=right时退出,这样,我们只需要检查退出循环时的nums[left]是否等于target,即可判断出target是否在nums中出现过。如果出现,必然返回的是第一次出现的索引,因为二分查找缩小的是右区间。对于查找最后一个出现的索引,同样的方法,令left=mid,即缩小左区间,根据返回值判断target是否出现过,若出现过,返回left,没出现,则返回-1
按照这个思路,具体代码如下:
public int[] searchRange(int[] nums, int target) {
if (nums ==null || nums.length==0){
return new int[]{-1,-1};
}
int firstPosition = findFirstPosition(nums, target);
if (firstPosition==-1){
return new int[]{-1,-1};
}
int lastPosition = findLastPosition(nums, target);
return new int[]{firstPosition,lastPosition};
}
//找出元素出现的第一个位置
public int findFirstPosition(int[] nums, int target){
int left=0;
int right=nums.length-1;
while (left<right){
int mid=(left +right)/2;
if(nums[mid]>target){
right=mid-1;
}
else if(nums[mid]<target){
left=mid+1;
}
else {
right=mid;
}
}
if (nums[left]==target) {
return left;
}
return -1;
}
//找出元素最后一个出现的位置
public int findLastPosition(int[] nums, int target){
int left=0;
int right=nums.length-1;
while (left<right){
int mid=(left +right+1)/2;
if(nums[mid]>target){
right=mid-1;
}
else if(nums[mid]<target){
left=mid+1;
}
else {
left=mid;
}
}
if (nums[left]==target){
return left;
}
return -1;
}
找出第一个大于目标元素的索引
nums = {1,3,5,5,6,6,8,9,11} target = 7
输出:6
大体思路仍是判断nums[mid]和target的大小然后进行区间缩减的操作,但这里要对nums[mid]>target的情况进行判断,因为nums[mid-1]若<=target,mid-1 ~ mid才是我们要找的目标区间,无脑让right =mid -1可能会丢掉正确的区间
同时注意对于等于的情况应放在小于里面进行判断,我们要找的是第一个大于目标元素的索引,等于的情况也依然要舍弃相应区间。
public static int lowBoundnum(int[] nums,int target,int left, int right) {
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] > target) {
if (mid == 0 || nums[mid-1] <= target) {
return mid;
}
else{
right = mid -1;
}
} else if (nums[mid] <= target){
left = mid + 1;
}
}
return -1;
}
第一个小于目标元素的索引
与前面的找出第一个大于目标值的索引相同,应对target<nums[mid]的情况进行判断,若nums[mid+1]>=target,则此区间为目标区间,不能无脑left=mid+1,可能丢掉目标区间。
同理,等于的情况放入大于中去,因为要找的是最后一个小于target的索引,等于的情况要舍弃区间。
public static int upperBoundnum(int[] nums,int target,int left, int right) {
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] < target) {
if (mid == right || nums[mid+1] >= target) {
return mid;
}
else{
left = mid + 1;
}
} else if (nums[mid] >= target){
right = mid - 1;
}
}
return -1;
}