二分查找
前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
以经典题为例:
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。力扣链接
示例:
输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4
方法一
定义target在左闭右闭的区间中,即[left,right]。
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
 - if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
 
示意图:

代码:
class Solution {public int search(int[] nums, int target) {int left = 0;int right = nums.length - 1;// 判断是否小于最小和大于最大if(target < nums[0] || target > nums[right]) {return -1;}// 左边界小于等于右边界while(left <= right) {int middle = (left + right) / 2;// 相等直接输出if(target == nums[middle]) {return middle;}// 小于则将右边界收缩else if(target < nums[middle]) {right = middle - 1;}// 大于则将右边界收缩else if(target > nums[middle]) {left = middle + 1;}}return -1;}}
方法二
定义target在左闭右开的区间中,即[left,right]。
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
 - if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
 
示意图:

代码:
class Solution {public int search(int[] nums, int target) {int left = 0;int right = nums.length;// 判断是否小于最小和大于最大if(target < nums[0] || target > nums[right-1]) {return -1;}// 左边界小于右边界while(left < right) {int middle = (left + right) / 2;// 相等直接输出if(target == nums[middle]) {return middle;}// 小于则将右边界收缩else if(target < nums[middle]) {right = middle; // 此时边界需要为[middle,right)}// 大于则将右边界收缩else if(target > nums[middle]) {left = middle + 1; // 此时边界需要为[middle+1,right)}}return -1;}}
注意:在left,right都为Integer.MAX_VALUE时会造成越界,所以可以使用int mid = left + ((right - left) / 2);代替 int middle = (left + right) / 2;
相关题目
搜索插入位置
题目描述:(力扣链接)
解题方法
解题流程:carl哥的解题流程
贴上代码:
方法一:
自己使用的时左闭右闭的写法,此处有四种情况:
- 目标值在数组中
 - 目标值在数组的范围
 - 目标值在数组范围的右侧
 - 目标值在数组范围的左侧
 
此处先使用经典的二分查找的方法,可以先判断出第一种情况,最后返回值为right + 1(注意此处包括了三种情况)
class Solution {public int searchInsert(int[] nums, int target) {int left = 0,right = nums.length - 1;// 循环判断while(left <= right) {int middle = (right + left) / 2;if(target == nums[middle]) {return middle;}else if(target < nums[middle]) {right = middle - 1;}else if(target > nums[middle]) {left = middle + 1;}}// 返回情况// 目标值在所有数组的后面right + 1// 目标值在数组中 middle// 目标值在数组前,// 此时经过循环之后,right在等于0时也会循环一次,所以最终循环之后right的结果为-1return right + 1;}}
方法二
暴力解法
int searchInsert(vector<int>& nums, int target) {for (int i = 0; i < nums.size(); i++) {// 分别处理如下三种情况// 目标值在数组所有元素之前// 目标值等于数组中某一个元素// 目标值插入数组中的位置if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果return i;}}// 目标值在数组所有元素之后的情况return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度}
在排序数组中查找元素的第一个和最后一个位置
题目描述:(力扣链接)

解题方法
解题流程:carl哥的解题流程
此处也有三种情况
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
 - 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
 - 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
 
class Solution {public int[] searchRange(int[] nums, int target) {int rightBorder = getRightBorder(nums, target);int leftBorder = getLeftBorder(nums, target);// 数字值范围超过数组,就是左边界在右边或者有边界在左边// 情况二if(rightBorder == -2 || leftBorder == -2) return new int[]{-1, -1};// 情况一if(rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1}; // 此处最后算出的right和left多减了一和多加了一// 情况三return new int[]{-1, -1};// 数字范围在数组中,但数组中无此值}public static int getRightBorder(int[] nums, int target) {int left = 0, right = nums.length - 1;int rightBorder = -2;// 寻找右边界while (left <= right) {int middle = (right + left) / 2;if(target < nums[middle]) {right = middle - 1;}else { // 当target==nums[middle]时,此时将left等于middle+1left = middle + 1;rightBorder = left; // 将left作为右边界}}return rightBorder;}public static int getLeftBorder(int[] nums, int target) {int left = 0, right = nums.length - 1;int leftBorder = -2;// 寻找左边界while (left <= right) {int middle = (right + left) / 2;if(target <= nums[middle]) {right = middle - 1; // 当target==nums[middle]时,此时将right等于middle+1leftBorder = right; // 将right作为左边界}else {left = middle + 1;}}return leftBorder;}}
X的平方根
题目描述(力扣链接)

解题方法:
此处也可以使用二分法,注意最后需要返回right,当middle平方等于x时直接返回,若没有等于,需要返回前一个值,此时right在最后被减了一,所以直接返回right即可。
public static int mySqrt(int x) {// 判断为0,1时直接返回if (x == 0 || x == 1) {return x;}int left = 1;int right = x / 2; // 直接将right赋值为x的二分之一,减少计算while (left <= right) {int middle = (right + left) / 2;// 使用除法代替乘法,防止使用乘法溢出if (middle == (x/middle)) {return middle;}else if (middle > (x / middle)) {right = middle - 1;}else if (middle < (x / middle)) {left = middle + 1;}}return right;}
有效的完全平方数
题目描述(力扣链接)

解题方法:
与上一题一致,但注意此处范围过大,需要使用long类型。right也可以从num/2开始查找,但是注意判断num==1的情况。
class Solution {public boolean isPerfectSquare(int num) {// 数据过大,使用long类型long left = 1;long right = num;while(left <= right) {long middle = (right + left) / 2;if(middle*middle == num) {return true;}else if(middle*middle < num) {left = middle + 1;}else if(middle*middle > num) {right = middle - 1;}}return false;}}
33. 搜索旋转排序数组
题目描述
解题思路
https://leetcode.cn/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/
注意要求的时间复杂度为O(log n),所以可以使用二分法。
但是直接使用二分不行,因为中间不是升序的。所以需要进行判断:

public int search(int[] nums, int target) {int n = nums.length;if (n == 0) return 0;if (n == 1) return nums[0] == target ? 0 : -1;int l = 0, r = n - 1;while (l <= r) {int mid = (l + r) / 2;if (nums[mid] == target) return mid;// 左边有序if (nums[0] <= nums[mid]) {if (nums[0] <= target && target <= nums[mid]) {r = mid - 1;} else {l = mid + 1;}} else {if (nums[mid] <= target && target <= nums[n - 1]) {l = mid + 1;}else {r = mid - 1;}}}return -1;}
