(学习参考书:LeetCode101)
二分查找也常被称为二分法或者折半查找,每次查找通过将待查找区间分成两部分并只取一部分继续查找。二分查找时区间的左右端取开区间还是闭区间在绝大部分时候都可以,但是熟练使用一种写法更好:如左闭右开或者左闭右闭。
力扣例题
69.Sqrt(x)
分析:
本题计算算数平方根,因此得数≥0,可以对[0,x]区间使用二分法找到对应值即可。因为小数部分舍去,即向下取整,在结束循环应该取右边的较小的一个边界,由于此时right<left,因此取right。
题解:
class Solution {
public int mySqrt(int x) {
if(x==0){
return 0;
}
int left = 1;
int right = x;
int mid,sqrt;
while (left<=right){
mid = (left+right)/2;
sqrt = x/mid;
if (sqrt==mid){
return mid;
}
else if(mid>sqrt){
right = mid-1;
}
else{
left = mid+1;
}
}
return right;
}
}
34.在排序数据中查找元素的第一个和最后一个位置
分析:
求target的第一个和最后一个位置即是求第一个等于target的位置和第一个大于target的位置-1。
定义两个辅助函数,分别找第一个位置和最后一个位置。
题解:
class Solution {
public int[] searchRange(int[] nums, int target) {
//如果为空数组直接返回-1
if (nums.length==0){
return new int[]{-1,-1};
}
//分别查找第一次和最后一次出现的位置
int lower = findLower(nums,target);
int upper = findUpper(nums,target)-1;
//如果查找结束 或者 查找出来的位置不是target仍要返回-1(低位都不是高位更不是)
if (lower== nums.length||nums[lower]!=target){
return new int[]{-1,-1};
}
return new int[]{lower,upper};
}
public int findLower(int[] nums,int target){
int left = 0;
int right = nums.length;
int mid;
while (left<right){
mid = (left+right)/2;
//因为这里是寻找第一次出现的位置因此条件为>=,
//目的是即使刚好nums[mid]==target,
//也可以继续往左边探查有没有更低位的与target相等的值
if (nums[mid]>=target){
right = mid;//这里要赋值为mid,而不是mid-1
}
else{
left = mid+1;
}
}
return left;
}
public int findUpper(int[] nums,int target){
int left = 0;
int right = nums.length;
int mid;
while (left<right){
mid = (left+right)/2;
//因为这里是寻找最后出现的位置因此条件为<=,
//目的是即使刚好nums[mid]==target,
//也要找到刚好比target大的值
if (nums[mid]<=target){
left = mid+1;//这里要赋值为mid+1,而不是mid
}
else{
//因为要找比target大的,所以不满足条件的
//即nums[mid]>target的mid也要作为新的右边界
//以防漏掉刚好比target大的数,即可能是当前的mid
right = mid;
}
}
return left;
}
}
81.搜索旋转排序数组2
分析:
即使数组被旋转过,但是仍可使用该数组的递增性,因为旋转过后分为两个区间的数组,在各自区间上仍然递增。对于当前的中点,如果小于右端值,则说明右区间是有序的;如果大于右端,说明左区间是有序的。如果目标数位于已经排好序的区间可以对该区间继续二分查找;反之,对于另一半区间继续二分查找找到有序的区间。
特殊情况,在nums[left]==nums[mid]==nums[right]时无法判断哪部分区间是有序的,但当前值又不是目标值,因此将left和right指针各移动一位。在新的二分区间的上进行查找。
题解:
class Solution {
public boolean search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = (left+right)/2;
if(nums[mid]==target){
return true;
}
if(nums[left]==nums[mid]&&nums[right]==nums[mid]){
left++;
right--;
}
else if(nums[mid]<=nums[right]){
if(target>nums[mid]&&target<=nums[right]){
left = mid+1;
}
else{
right = mid-1;
}
}
else{
if(target<nums[mid]&&target>=nums[left]){
right = mid-1;
}
else{
left = mid+1;
}
}
}
return false;
}
}
154.寻找旋转排序数组中的最小值2
分析:
旋转后的数组仍然是由两个有序数组组成的,由81题分析很容易判断最小值在左右区间的分布情况。如果nums[mid]>nums[right]说明最小值在右边的区间;如果nums[mid]<nums[right]说明最小值在左边的区间;如果遇到无法判断的情况,使用暴力向左缩小区间,即可找到最小值或跳出无法判断的情况的循环。
题解:
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length-1;
int mid;
if (nums.length==1){
return nums[0];
}
while(left<right){
mid = left+(right-left)/2;
if (nums[mid]>nums[right]){
//说明左区间有序且为大数区间
//最小值在右边的区间
left = mid+1;
}
else if(nums[mid]<nums[right]){
//说明右区间有序且为大数区间
//最小值在左边的区间
//这里也不确定mid是否对应最小值,因此下一次循环仍要比较
right = mid;
}
else{
//无法判断最小值所在的区间
right--;
}
}
return nums[left];
}
}
540.有序数组中的单一元素
分析:
由于数组是由一个单次出现的元素与若干个两次出现的元素组成,所以数组的长度为奇数。若中间的数就是唯一的元素则输出。如果中间的元素是两次出现的元素,则整个数组会被这两个相同的元素划分为一奇数长度一偶数长度的两个数组。
而唯一出现的元素一定在奇数数组中,如此反复缩小区间,最后剩下的元素必定是唯一出现的元素。
题解:
class Solution {
public static int singleNonDuplicate(int[] nums) {
int left = 0;
int right = nums.length-1;
int mid;
int flag;
if (right==0){
return nums[0];
}
while(left<right){
mid = left+(right-left)/2;
if (nums[mid]==nums[mid+1]){
//和mid相等的元素在mid的右边
flag = (mid-left)%2;
//判断奇数区间,并更新左右端点
if (flag==0){
left = mid+2;
}
else {
right = mid-1;
}
}
else if (nums[mid]==nums[mid-1]){
//和mid相等的元素在mid的左边
flag = (mid-1-left)%2;
//判断奇数区间,并更新左右端点
if (flag==0){
left = mid+1;
}
else {
right = mid-2;
}
}
else{
//mid对应的值是唯一值
return nums[mid];
}
}
return nums[left];
}
}