写在前面
- 文章是在前人的基础上进行总结整理再加上自己的一点理解,仅作为自己学习的记录,不作任何商业用途!
- 如果在文章中发现错误或者侵权问题,欢迎指出,谢谢!
- 相对于二分查找算法Ⅰ,这个版本的适合去解决更多的关于二分的题目
704. 二分查找
public int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
int right = nums.length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid;
// or left = mid + 1;
} else {
right = mid;
// or right = mid -1;
}
}
if (nums[left] == target) {
return left;
}
if (nums[right] == target) {
return right;
}
return -1;
}
-
Q1:为什么
mid = left + (right - left) / 2来计算 mid 的值?- 是为了防止溢出,当 left 和 right 都接近 2^31 的时候,那么使用
mid = (left + right) / 2的方式去计算就会导致 int 范围溢出
- 是为了防止溢出,当 left 和 right 都接近 2^31 的时候,那么使用
-
Q2:为什么使用
left + 1 < right这个条件?- 先说一个
left < right和left <= right这两种写法,前者是当left == right,即左右两个指针相交的时候退出循环,后者是left == right + 1,即左指针超过右指针退出循环 - 上面的两种方式 left 和 right 的更新逻辑没写好都有可能导致退不出循环,而
left + 1 < right则一定会退出循环 left + 1 < right退出循环的时候有 [left, right],这个区间内大多数情况下是有两个数,另外只有一个数的情况时 left 和 right 最开始就相等,也即数组只有一个元素
- 先说一个
-
Q3:那为什么
left + 1 < right这种方式一定可以退出循环呢?- 假设是使用
left < right这个条件,查找的是 target 最后一次出现的位置,并且left = 1,right = 2的时候,计算出mid = 1,因为是找最后一次出现的位置,所以会进入 while 循环里面的那个 if 分支的逻辑是:left = mid。那么这种情况就会导致 left 始终等于 1,mid 也始终等于 1,最终形成死循环 - 而使用
left + 1 < right这种方式呢,它在相邻(或相等)的时候就退出循环了,始终不会相交,就不会导致死循环
- 假设是使用
-
Q4:为什么当
nums[mid] == target的时候是right = mid,而不是return mid?- 这个题的逻辑实际上是求 target 第一次出现的位置,只是也能求 target 在不包含重复元素的数组中的位置
-
Q5:为什么 left 和 right 的更新逻辑是
left = mid和right = mid?- 实际上这道题也可以使用
left = mid + 1和right = mid - 1这个逻辑来更新,只是当碰到比如说:找第一个比 target 小的数,那么就不能left = mid + 1,这种情况就会跳过那个最小值,因此为了方便统一,就统一使用left = mid和right = mid,都不用纠结是否需要加 1 和减 1 了
- 实际上这道题也可以使用
-
Q6:为什么最后需要判断 nums[left] 和 nums[right]?
- 这也是因为
left + 1 < right这种写法导致的,最终退出循环的时候是没有去判断 left 和 right 位置的值的,所以退出循环后需要补刀
- 这也是因为
-
二分的核心思想:通过一个 while 循环将一个 n 大小的问题转换为 n/2 大小的问题,最终变成一个 O(1) 的问题,不要想着直接在 while 里面就将 target 找到,而是想着怎么去将查找范围缩小
34. 在排序数组中查找元素的第一个和最后一个位置
public int[] searchRange(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int[] bound = new int[2];
// 寻找左边界
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
if (nums[left] == target) {
bound[0] = left;
} else if (nums[right] == target) {
bound[0] = right;
} else {
bound[0] = bound[1] = -1;
return bound;
}
// 寻找右边界
left = 0;
right = nums.length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
if (nums[left] == target) {
bound[1] = left;
} else if (nums[right] == target) {
bound[1] = right;
} else {
bound[1] = bound[1] = -1;
return bound;
}
return bound;
}
69. x 的平方根
- 求 x 的平方根实际上能够使得
number^2 <= x成立的最后一个 number
public int sqrt(int x) {
int left = 1;
int right = x;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (mid * mid == x) {
return mid;
} else if (mid * mid < x) {
left = mid;
} else {
right = mid;
}
}
if (right * right <= x) {
return (int)right;
}
return (int)left
}
- 如果返回值是 double 呢?
- 如果是 double 的话,退出循环的条件就不是
left + 1 < right了,而是(right - left) > 1e-6
- 如果是 double 的话,退出循环的条件就不是
public double sqrt(int x) {
double left = 0;
double right = (double)x;
while ((right - left) > 1e-6) {
double mid = (left + right) / 2; // 可以直接相加除2,因为都是 double 类型
// ...
}
return left; // 也可以直接返回 left 或者 right 都行,因为相差很小
}
74. 搜索二维矩阵
-
思路1:一次二分
- 可以看作是一个有序数组被分成了 n 段,每段就是一行。因此依然可以二分求解。 对每个数字根据其下标 i,j 进行编号,每个数字可被编号为 0~n*n-1
- 相当于是在一个数组中的下标。然后直接像在数组中二分一样来做。取的mid要还原成二位数组中的下标,
i = mid / n, j = mid % n - 时间复杂度为:O(logn + logm)
-
思路2:两次二分
- 先通过二分找到 target 在哪一行
- 再通过二分找到在该行的哪一个
-
- 时间复杂度为:O(n + m)
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}
if (matrix[0] == null || matrix[0].length == 0) {
return false;
}
int left = 0;
int right = matrix.length * matrix[0].length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (getMatrixValue(matrix, mid) == target) {
return true;
} else if (getMatrixValue(matrix, mid) < target) {
left = mid;
} else {
right = mid;
}
}
if (getMatrixValue(matrix, left) == target) {
return true;
}
if (getMatrixValue(matrix, right) == target) {
return true;
}
return false;
}
private int getMatrixValue(int[][] matrix, int index) {
int row = index / matrix[0].length;
int column = index % matrix[0].length;
return matrix[row][column];
}
public boolean searchMatrix2(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}
if (matrix[0] == null || matrix[0].length == 0) {
return false;
}
int row = matrix.length;
int column = matrix[0].length;
int left = 0;
int right = row - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (matrix[mid][0] == target) {
return true;
} else if (matrix[mid][0] < target) {
left = mid;
} else {
right = mid;
}
}
// 要先对更下面的那行进行判断,保证判断 target 在哪一行不会出现问题
if (matrix[right][0] <= target) {
row = right;
} else if (matrix[left][0] <= target) {
row = left;
} else {
return false;
}
// 下面的查找逻辑就和 704 题没什么区别了
left = 0;
right = matrix[0].length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (matrix[row][mid] == target) {
return true;
} else if (matrix[row][mid] < target) {
left = mid;
} else {
right = mid;
}
}
if (matrix[row][left] == target) {
return true;
} else if (matrix[row][right] == target) {
return true;
}
return false;
}
240. 搜索二维矩阵 II
-
根据题意,每行中的整数从左到右是排序的,每一列的整数从上到下是排序的,在每一行或每一列中没有重复的整数。那么我们只要从矩阵的左下角开始向右上角找
- 从左下角即
(m - 1,0)处出发 - 如果
matrix[x][y] < target下一步往右搜 - 如果
matrix[x][y] > target下一步往上搜 - 如果
matrix[x][y] = target下一步往[x - 1][y + 1]即右上角搜,因为是有序的,每一行每一列中每个数都是唯一的
- 从左下角即
-
时间复杂度为 O(m + n)
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
int count = 0;
int x = m - 1;
int y = 0;
while (x >= 0 && y < n) {
if (matrix[x][y] == target) {
x--;
y++;
count++;
} else if (matrix[x][y] < target) {
y++;
} else {
x--;
}
}
return count != 0;
}
35. 搜索插入位置
public int searchInsert(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int left = 0;
int right = nums.length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
left = mid;
} else {
right = mid;
}
}
if (nums[left] >= target) {
return left;
} else if (nums[right] >= target) {
return right;
}
return nums.length; // target 比数组中的数都大
}
278. 第一个错误的版本
public int firstBadVersion(int n) {
int left = 1;
int right = n;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (isBadVersion(mid)) {
right = mid;
} else {
left = mid;
}
}
if (isBadVersion(left)) {
return left;
}
return right;
}
- 这道题就很好的体现了二分法的思想,二分法只是在不断的将搜索范围减小为原来的一半,在最后退出循环的几个 if 判断里面才是最终找出答案
153. 寻找旋转排序数组中的最小值
- 思路1:使用二分法
- 第一次分类讨论:比较
nums[left]和nums[right]- 如果
nums[left] < nums[right],说明数组没有旋转过,仍然是升序排列。我们直接return nums[left] - 反之,说明数组非单调,进入到第二次分类讨论
- 如果
- 第二次分类讨论:比较
nums[left]和nums[mid],其中mid是二分中点- 如果
nums[left] > nums[mid],可以证明此时数组右半边是升序的,那我们就不用考虑右半边了。最小值一定在[left, mid]间,令right = mid - 如果
nums[left] <= nums[mid],可以证明此时数组左半边是升序的,于是我们不需要考虑左半边,令left = mid或者left = mid + 1也行
- 如果
- 第一次分类讨论:比较
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left + 1 < right) {
if (nums[left] < nums[right] {
return nums[left];
}
int mid = left + (right - left) / 2;
if (nums[left] > nums[mid]) {
right = mid;
} else {
left = mid;
}
}
return Math.min(nums[left], nums[right]);
}
- 思路2:使用二分法
- 每次
nums[mid]和nums[right]比较,得到的结果也是正确的 - 虽然遍历过程不一定会相同,但两个思路是对偶的
- 每次
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid;
} else {
right = mid;
}
}
return Math.min(nums[left], nums[right]);
}
154. 寻找旋转排序数组中的最小值 II
-
和 153 题的区别在于:会出现重复的元素,我们依旧使用旋转数组的特性,用改进后的二分查找来解决,只是因为重复元素的存在,分类讨论的条件更加精确了
-
思路1:
nums[left] 和 nums[mid] 比较- 第一次分类讨论:比较
nums[left]和nums[right]- 如果
nums[left] < nums[right],说明数组没有旋转过,仍然是升序排列。我们直接return nums[left]。 - 反之,说明数组非单调,进入到第二次分类讨论
- 如果
- 第二次分类讨论:比较
nums[left]和nums[mid],其中mid是二分中点- 如果
nums[left] > nums[mid],可以证明此时数组右半边是非严格升序的,那我们就不用考虑右半边了。最小值一定在[left, mid]间,令right = mid - 如果
nums[left] < nums[mid],可以证明此时数组左半边是非严格升序的,那我们就不用考虑左半边了。最小值一定在[mid, right]间,令left = mid或者left = mid + 1也行 - 如果
nums[left] == nums[mid],这就是这道题和 153 题的区别:- 这时我们无法确定最小元素出现在哪里,举例来说,
1,1,1,3,4和3,3,3,1,2,最左值和中间值都相等,最小值前者出现在左半边,后者出现在右半边。所以我们无法进行二分,采用的方法是left += 1,可以证明这种做法不会使最小值丢失。然后在新的区间继续寻找
- 这时我们无法确定最小元素出现在哪里,举例来说,
- 如果
- 第一次分类讨论:比较
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left + 1 < right) {
if (nums[left] < nums[right]){
return nums[left];
}
int mid = left + (right - left) / 2;
if (nums[left] == nums[mid]) {
left += 1;
}else if (nums[left] > nums[mid]) {
right = mid;
} else {
left = mid;
// or left = mid + 1;
}
}
return Math.min(nums[left], nums[right]);
}
- 思路2:
nums[mid] 和 nums[right] 比较- 和思路1的区别在于:
nums[left] == nums[mid],令right -= 1
- 和思路1的区别在于:
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == nums[right]) {
right -= 1;
} else if (nums[mid] > nums[right]) {
left = mid;
} else {
right = mid;
}
}
return Math.min(nums[left], nums[right]);
}
- 经过对比发现,不论是 153 题还是 154 题,都是思路2比较方便
33. 搜索旋转排序数组
- 思路1:将
nums[mid]和nums[left]比较
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid = 0;
while (left + 1 < right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[left] < nums[mid]) { // [left, mid] 是升序
if (nums[left] <= target && target < nums[right]) {
right = mid;
} else {
left = mid;
}
} else {
if (nums[mid] < target && target <= nums[right]) {
left = mid;
} else {
right = mid;
}
}
}
if (nums[left] == target) {
return left;
}
if (nums[right] == target) {
return right;
}
return -1;
}
- 思路2:将
nums[mid]和nums[right]比较
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid = 0;
while (left + 1< right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[mid] < nums[right]) { // [mid, right] 是升序
if (nums[mid] < target && target <= nums[right]) {
left = mid;
} else {
right = mid;
}
} else {
if (nums[left] <= target && target < nums[mid]) {
right = mid;
} else {
left = mid;
}
}
}
if (nums[left] == target) {
return left;
}
if (nums[right] == target) {
return right;
}
return -1;
}
81. 搜索旋转排序数组 II
- 思路1:将
nums[mid]和nums[left]比较
public boolean search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid = 0;
while (left + 1 < right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[left] < nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid;
} else {
left = mid;
}
} else if (nums[mid] < nums[left]) {
if (nums[mid] < target && target <= nums[right]) {
left = mid;
} else {
right = mid;
}
} else {
left += 1;
}
}
if (nums[left] == target) {
return true;
}
if (nums[right] == target) {
return true;
}
return false;
}
- 思路2:将
nums[mid]和nums[right]比较
public boolean search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int mid = 0;
while (left + 1 < right) {
mid = left + (right - left) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[mid] < nums[right]) {
if (nums[mid] < target && target <= nums[right]) {
left = mid;
} else {
right = mid;
}
} else if (nums[mid] > nums[right]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid;
} else {
left = mid;
}
} else {
right -= 1;
}
}
if (nums[left] == target) {
return true;
}
if (nums[right] == target) {
return true;
}
return false;
}
852. 山脉数组的峰顶索引
public int peakIndexInMountainArray(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > arr[mid + 1]) {
right = mid;
} else {
left = mid ;
}
}
if (arr[left] < arr[right]) {
return right;
}
return left;
}
1095. 山脉数组中查找目标值
- 思路:分三个步骤完成
- 先找到 peakIndex,将数组分为两个有序部分,左半部分是升序,右半部分是降序
- 在左半部分通过二分查找,如果找到了则直接 return,因为是找最小的那个位置
- 在右半部分通过二分查找,注意这里是降序查找
public int findInMountainArray(int target, MountainArray mountainArr) {
int left = 0;
int right = mountainArr.length() - 1;
int peekIndex = peakIndexInMountainArray(mountainArr, left, right);
int rightIndex = binarySearchLeftPath(mountainArr, left, peekIndex, target);
if (rightIndex != -1) {
return rightIndex;
}
int leftIndex = binarySearchRightPart(mountainArr, peekIndex + 1, right, target);
return leftIndex;
}
public int binarySearchLeftPath(MountainArray mountainArr, int left, int right, int target) {
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (mountainArr.get(mid) == target) {
return mid;
} else if (mountainArr.get(mid) < target) {
left = mid;
} else {
right = mid;
}
}
if (mountainArr.get(left) == target) {
return left;
}
if (mountainArr.get(right) == target) {
return right;
}
return -1;
}
public int binarySearchRightPart(MountainArray mountainArr, int left, int right, int target) {
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (mountainArr.get(mid) == target) {
return mid;
} else if (mountainArr.get(mid) > target) { // 将 < 改为 > 即可实现降序查找
left = mid;
} else {
right = mid;
}
}
if (mountainArr.get(left) == target) {
return left;
}
if (mountainArr.get(right) == target) {
return right;
}
return -1;
}
public int peakIndexInMountainArray(MountainArray mountainArr, int left, int right) {
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (mountainArr.get(mid) > mountainArr.get(mid + 1) ) {
right = mid;
} else {
left = mid ;
}
}
if (mountainArr.get(left) < mountainArr.get(right) ) {
return right;
}
return left;
}