LeetCode分类专题(三)——二分查找1
iwehdio的博客园:https://www.cnblogs.com/iwehdio/
学习自:
-
本文包括LeetCode:29、33、34、35、69、50、74、81、153
1、二分搜索
- 二分搜索
- 首先,二分搜索的思想是,每次减少问题的一半规模。对于有序数组而言,是通过每次比较mid的值与目标值的大小而定的。
- 根据右边界是闭区间还是开区间,可以分为两种。区别在于:
- 右边界的初始值是length还是length-1。
- 结束条件是left < right还是left <= right。
- 改变右边界时是right = mid还是right = mid - 1。
- 一般来说,选取闭区间的程序可读性较好,所以一般选第二种。
- 查找有序数组中的元素:
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意
while(left < right) { // 注意
int mid = (right + left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid; // 注意
}
return -1;
}
//-----------------------------------------
int binarySearch(int[] nums,int target) {
int left = 0;
int right = nums.length-1; // 注意
while (left <= right) { // 注意
int mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
}else if (nums[mid] < target) {
left = mid + 1;
}else if (nums[mid] > target) {
right = mid - 1; // 注意
}
}
return -1
}
-
对于右边界为length-1的情况而言:
- left/right左右边界对应的就是最左和最右元素的索引。
- nums[mid] < target就意味着mid的左侧都比target小,那么target可能落在
[mid,hi]
,但是mid如果单独判断过的话,就是[mid+1,hi]
,所以left赋值为mid+1,即left左侧(不包含)都比target小。 - 相应的,right右侧(不包含)都比target大,所以right赋值为mid-1。
- 结束条件是left <= right。这意味着,最后一次循环时,left与right是重合的。此时,只需要判断这个重合的位置mid是否等于target。如果等于就找到返回。
- 如果不等于,就应该返回插入位置。插入位置的含义是,数组中大于target的最小值。
- 如果mid小于target,说明left应该右移一位,插入位置在left。
- 如果mid大于target,说明right应该左移一位,插入位置还是在left。
-
查找有序数组中的元素并返回插入位置:
int binarySearch(int[] nums,int target) {
int left = 0;
int right = nums.length-1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
}else if (nums[mid] < target) {
left = mid + 1;
}else if (nums[mid] > target) {
right = mid - 1;
}
}
return left;
}
-
如果数组中存在重复元素,那么目标就成了搜索重复元素的最左或最右端元素。如果是找做边界,在这种情况下,left的含义还是左侧(不包含)都比target小,而right的含义变为了right右侧(不包含)都大于等于target,此时target=mid的情况归入right。
-
因此,最后的情况就是,最后一次循环中,重合的mid要么是target的左边界,要么是左边界的往左一个元素。因为只有这两种情况才能同时符合left和right的定义。
-
计算左右边界,二者唯一的区别就是等号被归入了不同的情况:
int leftBound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (target <= nums[mid]) {
right = mid - 1;
}else if (target > nums[mid]) {
left = mid + 1;
}
}
return left;
}
//------------------------------------------------------
int rightBound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (target >= nums[mid]) {
left = mid + 1;
}else if (target < nums[mid]) {
right = mid - 1;
}
}
return left;
}
2、旋转数组
-
旋转数组
- 旋转数组,是由有序数组从一个点旋转而来的,也可以用二分查找来查找元素。原因是,二分查找只是要求每次减少一半规模,如果不是完全有序的话,不能只根据mid与target的大小关系,需要借助其他条件。
- 根据二分的思想,应该得出target在mid的左侧或者右侧。在无重复元素的情况下:
- 如果mid大于left,那么mid左侧是有序的,可以得出:
- 如果left<target<mid,那么target在mid左侧,否则在右侧。
- 如果mid小于left,那么mid右侧是有序的,同样可以得出:
- 如果mid<target<right,那么target在mid右侧,否则在左侧。
- 如果mid大于left,那么mid左侧是有序的,可以得出:
class Solution {
public int search(int[] nums, int target) {
int lo = 0, hi = nums.length-1;
while(lo<=hi) {
int mid = (lo+hi)/2;
if(nums[mid]==target) {
return mid;
} else if(nums[mid]>=nums[lo]) {
if(target>=nums[lo] && target<nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else {
if(target>nums[mid] && target<=nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return -1;
}
}
- 如果旋转数组中有重复元素的话,就可能出现mid与left相等的情况,这样就没法判断是左边有序还是右边有序了,需要移动左边界一直到左边界与mid不等。
class Solution {
public boolean search(int[] nums, int target) {
int lo=0, hi = nums.length-1;
while(lo<=hi) {
int mid = (lo+hi)/2;
if(nums[mid]==target) {
return true;
}
if(nums[mid]==nums[lo]) {
lo++;
continue;
}
if(nums[mid]<nums[lo]) {
if(target>nums[mid] && target<=nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
} else {
if(target<nums[mid] && target>=nums[lo]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
}
}
return false;
}
}
- 查找旋转数组中的最小值,这种情况下延续之前的思想,比较mid与left:
- 如果mid小于left,则
[mid,right]
是有序的,最小值可能在[left,mid]
中。 - 如果mid大于等于left,则
[left,mid]
是有序的,最小值可能在[mid+1,right]
中。 - 需要注意的是退化情况,即如果数组是有序的,直接返回left。这通过比较left和right来决定。
- 如果mid小于left,则
- 为什么left=mid+1,而right=mid?
- 因为mid右侧有序,那么mid本身可能是最小值,但是如果mid左侧有序,那么mid必定不是最小值。
- 为什么=mid的情况归到了left?
- 因为如果出现=的情况,一定是区间只存在left/mid和right两个元素,而又没有left<right自然有序,最小值就是right了。
class Solution {
public int findMin(int[] nums) {
int lo = 0, hi = nums.length-1;
while(lo<=hi) {
if(nums[lo]>nums[hi]) {
int mid = (lo+hi)/2;
if(nums[mid]>=nums[lo]) {
lo = mid+1;
} else {
hi = mid;
}
} else {
return nums[lo];
}
}
return nums[lo];
}
}
3、题目
-
29 两数相除
- 这个题的如果使用一次次减来等效乘法,时间复杂度太高了,所以需要倍数递增。
- 比如19/6,19比6大,然后6倍增为12;19比12大,然后12倍增为24;19比24小,则19/6的商为:倍数2 + (19-12)/6。递归的执行。
- 所以,这里div函数的含义是,计算出x<y*2^n中n的最大值(x、y为负数)。
- 这里比较坑的是,需要考虑int类型的溢出问题。数值为-231~231-1,也就是说,整数比负数少一个,因此可以全部转为负数来规避溢出。
- 此外,在倍增过程中,如果当前的减数大于Integer.MIN_VALUE=-2^31,也不能再递增了,否则就会溢出。而最后的结果,如果是Integer.MIN_VALUE,则其相反数会溢出,需要返回Integer.MAX_VALUE。
- 溢出的结果是Integer.MAX_VALUE+1=,Integer.MIN_VALUE-1=Integer.MAX_VALUE。
class Solution { int ans = 0; public int divide(int dividend, int divisor) { //全部转为负数 boolean flag = false; if((dividend<0 && divisor>0)||(dividend>0 && divisor<0)) { flag = true; } if(dividend>0) dividend = -dividend; if(divisor>0) divisor = -divisor; //递归除法 while(dividend <= divisor) { dividend = div(dividend, divisor); } //根据flag复原结果 if(flag) { return -ans; } else { //如果溢出 if(ans == Integer.MIN_VALUE) return Integer.MAX_VALUE; return ans; } } //倍增除法 public int div(int x, int y) { int res = 1; while(x <= (y << 1)){ if(y <= (Integer.MIN_VALUE >> 1)) break; y <<= 1; res <<= 1; } ans += res; return x - y; } }
-
33 搜索旋转排序数组
-
二分查找的本质甚至不是有序,而是每次减半规模,也就是分治。在这种情况下,每次减少一半的问题规模,就是看目标元素是在中间元素的左边还是右边。
-
具体的来说,是跟边界元素、中间元素和目标元素的大小关系有关。
-
如果中间元素大于等于左边界元素:
- 如果左边界元素<=目标元素<中间元素,则目标元素在中间元素的左侧。
- 否则,在右侧。
-
如果中间元素小于左边界元素:
- 如果中间元素<目标元素<=右边界元素,则目标元素在中间元素的右侧。
- 否则,在左侧。
class Solution { public int search(int[] nums, int target) { int lo = 0, hi = nums.length-1; while(lo<=hi) { int mid = (lo+hi)/2; if(nums[mid]==target) { return mid; } else if(nums[mid]>=nums[lo]) { if(target>=nums[lo] && target<nums[mid]) { hi = mid - 1; } else { lo = mid + 1; } } else { if(target>nums[mid] && target<=nums[hi]) { lo = mid + 1; } else { hi = mid - 1; } } } return -1; } }
-
-
34 在排序数组中查找元素的第一个和最后一个位置
- 首先用一个二分查找找到目标元素,然后再用两个二分查找搜索左右边界。
- 对于返回值,二分搜索最后一定是hi在lo左侧终止,即hi+1=lo。
- findleft,lo的含义是小于target的都在lo左侧,而大于等于target的都在hi右侧,因此左边界是lo。
- findright,小于等于target的都在lo左侧,大于hi的都在hi右侧,因此返回hi。
class Solution { int left = -1; int right = -1; public int[] searchRange(int[] nums, int target) { int lo = 0, hi = nums.length-1; while(lo<=hi) { int mid = (lo+hi)/2; if(nums[mid]==target) { findleft(nums, target, lo, hi); findright(nums, target, lo, hi); break; } else if(nums[mid]>target) { hi = mid - 1; } else { lo = mid + 1; } } return new int[]{left,right}; } public void findleft(int[] nums, int target, int lo, int hi) { while(lo<=hi) { int mid = (lo+hi)/2; if(nums[mid]>=target) { hi = mid - 1; } else if(nums[mid]<target) { lo = mid + 1; } } left = lo; } public void findright(int[] nums, int target, int lo, int hi) { while(lo<=hi) { int mid = (lo+hi)/2; if(nums[mid]<=target) { lo = mid + 1; } else if(nums[mid]>target) { hi = mid - 1; } } right = hi; } }
-
35 搜索插入位置
- 二分查找,重要的是找不到的情况下的返回值。初始值hi设置为数组长度,避免了单独处理大于数组中所有数的问题。
- 最后当区间长度为1时,如果目标元素小于则就在mid处插入,否则在mid+1处。
class Solution { public int searchInsert(int[] nums, int target) { int lo = 0, hi = nums.length-1; while(lo<=hi) { int mid = (lo+hi)/2; if(nums[mid]==target) { return mid; } else if(nums[mid]>target) { hi = mid - 1; } else { lo = mid + 1; } } return lo; } }
-
69 x的平方根
- 二分查找[0,x]区间内的值。坑的点主要有2:
- 输入的最大值为int的上限,此时mid*mid可能会溢出,需要转换为long。
- x的平方根可能为x自己,所以搜索应包含x。如果以lo<hi为终止条件,即区间右边界为不包含,起始值hi应为x+1,可能会溢出。所以需要以lo<=hi为终止条件,起始值为x。
- 因为去掉小数部分,即平方小于x,返回值为lo-1,即小于平方根的最大整数。
class Solution { public int mySqrt(int x) { int lo=0, hi = x; while(lo<=hi) { int mid = lo + (hi-lo)/2; if((long)mid*mid==x) { return mid; } if(x<(long)mid*mid) { hi = mid - 1; } else { lo = mid + 1; } } return lo-1; } }
- 二分查找[0,x]区间内的值。坑的点主要有2:
-
50 Pow(x,n)
- 本题的问题仍然是负数最大值大于正数,所以转换时要改为long。
class Solution { public double myPow(double x, int n) { boolean isNeg = false; long N = (long)n; if(N<0) { isNeg = true; N = -N; } double ans = 1; while(N>0) { if((N&1)==1) { ans *= x; } N >>= 1; x *= x; } if(isNeg) { return 1/ans; } else { return ans; } } }
-
74 搜索二维矩阵
- 先用一个二分搜索找出所在的行,即小于等于目标元素的最大值,返回lo-1/hi。
- 这里需要校验lo-1的合法性,因为可能目标元素小于第一行的最小值,这样就直接返回false了。
- 然后再用一个普通的二分查找找出所在的列。
class Solution { public boolean searchMatrix(int[][] matrix, int target) { int mlo=0, mhi=matrix.length-1; while(mlo<=mhi) { int mmid = (mlo+mhi)/2; if(target<matrix[mmid][0]) { mhi = mmid - 1; } else { mlo = mmid + 1; } } int index = mhi; if(index<0) return false; int lo=0, hi=matrix[index].length-1; while(lo<=hi){ int mid = (lo+hi)/2; if(target==matrix[index][mid]) { return true; } if(target<matrix[index][mid]) { hi = mid - 1; } else { lo = mid + 1; } } return false; } }
-
81 搜索旋转排序数组Ⅱ
- 与之前的旋转数组不同的地方主要在于,可能会出现重复元素。
- 出现重复元素,可能会导致无法根据左边界和 mid的大小关系判断旋转点在mid的哪一边(比如数组[1,0,1,1,1]),需要逐个缩减边界。
- 需要注意的是target和左边界或者右边界比较时,需要加=号。
class Solution { public boolean search(int[] nums, int target) { int lo=0, hi = nums.length-1; while(lo<=hi) { int mid = (lo+hi)/2; if(nums[mid]==target) { return true; } if(nums[mid]==nums[lo]) { lo++; continue; } if(nums[mid]<nums[lo]) { if(target>nums[mid] && target<=nums[hi]) { lo = mid + 1; } else { hi = mid - 1; } } else { if(target<nums[mid] && target>=nums[lo]) { hi = mid - 1; } else { lo = mid + 1; } } } return false; } }
-
153 寻找旋转排序数组中的最小值
- 首先看数组是不是有序的(通过比较左边界和右边界的大小),如果是有序的,就返回左边界。
- 否则,看mid与左边界(nums[mid]>=nums[lo])或右边界(nums[mid]<nums[hi])的大小关系,减少一半规模。
class Solution {
public int findMin(int[] nums) {
int lo = 0, hi = nums.length-1;
while(lo<=hi) {
if(nums[lo]>nums[hi]) {
int mid = (lo+hi)/2;
if(nums[mid]>=nums[lo]) {
lo = mid+1;
} else {
hi = mid;
}
} else {
return nums[lo];
}
}
return nums[lo];
}
}
iwehdio的博客园:https://www.cnblogs.com/iwehdio/
来源与结束于否定之否定。