二分搜索
1. 寻找一个数
采用左右均闭区间
int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 注意 while(left <= right) { int mid = left + (right - left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] < target) left = mid + 1; // 注意 else if (nums[mid] > target) right = mid - 1; // 注意 } return -1; }
搜索区间两端均为闭区间,所以while循环为 while left ≤ right
while(left <= right)的终止条件是left == right + 1,写成区间的形式就是[right + 1, right],或者带个具体的数字进去[3, 2],可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
采用左闭右开区间
int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length // 注意 区间为[left, right) while(left < right) { int mid = left + (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; }搜索区间两端均为闭区间,所以while循环为 while left ≤ right while(left <= right)的终止条件是left == right + 1,写成区间的形式就是[right + 1, right],或者带个具体的数字进去[3, 2],可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
搜索区间为左闭右开,所以while循环为 while left < right
while(left < right)的终止条件是left == right ,写成区间的形式就是[right , right],因为right始终不在搜索区间内,所以这时候 while 循环终止是正确的,直接返回 -1 即可。
2. 寻找左侧边界
左闭右开
int left_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0; int right = nums.length; // 注意 while (left < right) { // 注意 int mid = (left + right) / 2; if (nums[mid] == target) { right = mid; } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; // 注意 } } if (left == nums.length) return -1; // 类似之前算法的处理方式 return nums[left] == target ? left : -1; }
和之前最大的区别在于,当nums[mid]=target时,不是直接返回mid,而是缩小搜索区间,因为要找左边界,所以应该是right = mid
1、为什么 while 中是<而不是<=?
因为right = nums.length而不是nums.length - 1。因此每次循环的「搜索区间」是[left, right)左闭右开。while(left < right)终止的条件是left == right,此时搜索区间[left, left)为空,所以可以正确终止。
2、最后为什么返回是这样呢?
先理解一下left代表的含义:

对于这个数组,算法会返回 1。这个 1 的含义可以这样解读:
nums
中小于 2 的元素有 1 个。比如对于有序数组
nums = [2,3,5,7]
target = 1
,算法会返回 0,含义是:nums
中小于 1 的元素有 0 个。再比如说
nums = [2,3,5,7], target = 8
,算法会返回 4
,含义是:nums中小于 8 的元素有 4 个。综上可以看出,函数的返回值(即left变量的值)取值区间是闭区间
[0, nums.length]
,所以我们简单添加两行代码就能在正确的时候 return -1
:所以当left== nums.leght时,不是nums里面所有元素均小于target,所以此时返回-1
最后再判断nums[left] == target的情况。
左右均闭
int left_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; // 搜索区间为 [left, right] while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { // 搜索区间变为 [mid+1, right] left = mid + 1; } else if (nums[mid] > target) { // 搜索区间变为 [left, mid-1] right = mid - 1; } else if (nums[mid] == target) { // 收缩右侧边界 right = mid - 1; } } // 检查出界情况 if (left >= nums.length) return -1; return nums[left] == target? left: -1; }
1, 为什么最后要检查出界情况
由于 while 的退出条件是left == right + 1,所以当target比nums中所有元素都大时,会存在以下情况使得索引越界:

3. 寻找右侧边界
左闭右开
int right_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0, right = nums.length; while (left < right) { int mid = (left + right) / 2; if (nums[mid] == target) { left = mid + 1; // 注意 } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; } } if (left == 0) return -1; return nums[left-1] == target ? (left-1) : -1; }
左右均闭
int right_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid - 1; } else if (nums[mid] == target) { // 这里改成收缩左侧边界即可 left = mid + 1; } } // 这里改为检查 right 越界的情况,见下图 if (right < 0 ) return -1; return nums[right] == target? right :-1; }
当target比所有元素都小时,right会被减到 -1,所以需要在最后防止越界:

总结
第一个,最基本的二分查找算法:
因为我们初始化 right = nums.length - 1 所以决定了我们的「搜索区间」是 [left, right] 所以决定了while (left <= right) 同时也决定了 left = mid+1 和 right = mid-1 因为我们只需找到一个 target 的索引即可 所以当 nums[mid] == target 时可以立即返回
第二个,寻找左侧边界的二分查找:
因为我们初始化 right = nums.length 所以决定了我们的「搜索区间」是 [left, right) 所以决定了while (left < right) 同时也决定了 left = mid + 1 和 right = mid 因为我们需找到 target 的最左侧索引 所以当 nums[mid] == target 时不要立即返回 而要收紧右侧边界以锁定左侧边界
第三个,寻找右侧边界的二分查找:
因为我们初始化 right = nums.length 所以决定了我们的「搜索区间」是 [left, right) 所以决定了while (left < right) 同时也决定了 left = mid + 1 和 right = mid 因为我们需找到 target 的最右侧索引 所以当 nums[mid] == target 时不要立即返回 而要收紧左侧边界以锁定右侧边界 又因为收紧左侧边界时必须 left = mid + 1 所以最后无论返回 left 还是 right,必须减一
对于寻找左右边界的二分搜索,常见的手法是使用左闭右开的「搜索区间」,我们还根据逻辑将「搜索区间」全都统一成了两端都闭,便于记忆,只要修改两处即可变化出三种写法:
intbinary_search(int[] nums,int target) { int left = 0, right = nums.length - 1; while(left <= right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; }elseif (nums[mid] > target) { right = mid - 1; }elseif(nums[mid] == target) { // 直接返回 return mid; } } // 直接返回 return -1; } intleft_bound(int[] nums,int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; }elseif (nums[mid] > target) { right = mid - 1; }elseif (nums[mid] == target) { // 别返回,锁定左侧边界 right = mid - 1; } } // 最后要检查 left 越界的情况 if (left >= nums.length || nums[left] != target) return -1; return left; } intright_bound(int[] nums,int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] < target) { left = mid + 1; }elseif (nums[mid] > target) { right = mid - 1; }elseif (nums[mid] == target) { // 别返回,锁定右侧边界 left = mid + 1; } } // 最后要检查 right 越界的情况 if (right < 0 || nums[right] != target) return -1; return right; }