1.搜索插入位置(难度等级:简单)
这道题也算是二分法的经典题了,其实逻辑很简单,最关键的是边界条件的判断。
class Solution {
public int searchInsert(int[] nums, int target) {
int start = 0;
int end = nums.length-1;
int res = nums.length;
while(start<=end){
int mid = ((end - start) >> 1) + start;
if(nums[mid]>=target){
res = mid;
end = mid - 1;
}else{
start = mid + 1;
}
}
return res;
}
}
2.搜索二维矩阵(难度等级:中等)
这道题其实就是上一道题的升级版,写法都是类似的~这里还有一个小技巧,将矩阵每一行拼接在上一行的末尾,则会得到一个升序数组,我们可以在该数组上二分找到目标元素。代码实现时,可以二分升序数组的下标,将其映射到原矩阵的行和列上。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int low = 0, high = m * n - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int x = matrix[mid / n][mid % n];
if (x < target) {
low = mid + 1;
} else if (x > target) {
high = mid - 1;
} else {
return true;
}
}
return false;
}
}
3. 在排序数组中查找元素的第一个和最后一个位置(难度等级:中等)
这道题我觉得沿着第一题思路就可以了。先找到数组中一个与target相等的元素,再在其左右寻找有没有其他相同元素,如果有及时更新结果即可~·
class Solution {
public int[] searchRange(int[] nums, int target) {
int n=nums.length;
int left=0,right=n-1;
int mid=0;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target){
int l=mid,r=mid;
while(l>=0&&nums[l]==target) l--;
while(r<n&&nums[r]==target) r++;
return new int[]{l+1,r-1};
}else if(nums[mid]<target){
left=mid+1;
}else{
right=mid-1;
}
}
return new int[]{-1,-1};
}
}
4.搜索旋转排序数组(难度等级:中等)
这道题和第一题相比又增加了一个考虑维度,即数组不是有序的。这个时候我们还想用二分法,那么我们必须自己构造有序数组。看到一个非常好的解释:
- 将数组一分为二。(其中有一个一定是有序的;另一个则是有序或部分有序的)
- 此时如果target在有序部分里,则用二分法查找。
- 否则进入无序部分查找,返回1。
public int search(int[] nums, int target) {
if(nums == null || nums.length == 0){
return -1;
}
int start = 0;
int end = nums.length - 1;
while (start <= end){
int mid = start + (end -start)/2;
if (nums[mid] == target){
return mid;
}
//后半部分有序
if(nums[mid] < nums[end]){
if(nums[mid] < target && target <= nums[end]){
start = mid + 1;
} else {
end = mid - 1;
}
} else {
if(nums[mid] > target && target >= nums[start]){
end = mid - 1;
} else {
start = mid + 1;
}
}
}
return -1;
}
也就是说,我们比1多判断一步,就是当前找的范围是否是有序的,并且永远只在有序的部分进行二分查找~
5.寻找旋转排序数组中的最小值(难度等级:中等)
这道题我认为就是寻找“断崖点”。仔细观察案例,你会发现旋转后的数组还是被分为2个有序数组,但是他们之间可能不是有序的,只要找到了这个点,就找到了答案。具体可以分为几种情况:
- 左值 < 中值, 中值 < 右值 :没有旋转,最小值在最左边,可以收缩右边界
- 左值 > 中值, 中值 < 右值 :有旋转,最小值在左半边,可以收缩右边界
- 左值 < 中值, 中值 > 右值 :有旋转,最小值在右半边,可以收缩左边界
- 左值 > 中值, 中值 > 右值 :单调递减,不可能出现
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) { //左半边有序,断点在右边
left = mid + 1;
} else {
right = mid; //右半边有序,断点在左边
}
}
return nums[left];
}
}
6.寻找两个正序数组的中位数(难度等级:困难)
本题最简单的思路就是将两个数组合并,然后求中位数。但是这种方法时间复杂度和空间复杂度都无法满足题目要求呀!这里不妨采用“排除”的思路,让我们来看这种情况:比较两个数组的第 k/2
个数字(如果 k
是奇数,向下取整),哪一个数组第k/2的数字更小,表明这个数组0~k/2的元素一定不是中位数了,这时候我们就可以将这些数排除!然后,我们反复进行如上相同的操作,不断排除数字,就能得到最终结果~
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
int left = (n + m + 1) / 2;
int right = (n + m + 2) / 2;
//将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;
}
private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
int len1 = end1 - start1 + 1;
int len2 = end2 - start2 + 1;
//让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1
if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
if (len1 == 0) return nums2[start2 + k - 1];
if (k == 1) return Math.min(nums1[start1], nums2[start2]);
int i = start1 + Math.min(len1, k / 2) - 1; //取当前K值下的第一个数组的中间元素索引
int j = start2 + Math.min(len2, k / 2) - 1; //取当前K值下的第二个数组的中间元素索引
if (nums1[i] > nums2[j]) {
return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
}
else {
return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
}
}
}
【总结】
二分法的使用场景往往是题目中出现 有序数组 的时候,其核心代码的逻辑其实就是1.中的逻辑,其他问题都是在1的基础上做了更多的考虑,只要掌握每个题巧妙的思路,就能解答了~