一、应用场景
- 寻找一个数是否在数组中
- 寻找该数在数组中的最左边界
- 寻找该数在数组中的最右边界
二、算法框架
int binarySearch(int[] nums, int target) {
//数组判断
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
//区间选择:左闭右闭|左闭右开
int right = ...;
//区间不同,判断条件不同,不过要求是不能漏过数组中的每个元素
//采用多个 if else 为了保证逻辑清晰
while (...){
//防止 left 与 right 过大,相加之后直接整数溢出
int mid = left + (right - left) / 2;
//不同目的进行不同的操作
if (nums[mid] > target) {
right = ...;
} else if (num[mid] == target) {
...;
} else if (num[mid] < target) {
left = ...;
}
}
//循环外的操作
...;
return ...;
}
三、具体算法实现
- 寻找一个数是否在数组中
这个场景是最简单的,搜索一个数是否在数组中,存在的话返回数组下标索引,不存在的话返回-1。
int binarySearch(int[] nums, int target) {
//数组判断
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
//左闭右闭
int right = nums.length - 1;
//注意循环条件
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (num[mid] == target) {
return mid;
} else if (num[mid] < target) {
left = mid + 1;
}
}
//退出循环没有返回表示在数组中不存在
return -1;
}
- 寻找该数在数组中的最左边界
该算法有两种实现方法,主要是区别在区间的闭合情况:左闭右闭和左闭右开。
左闭右闭
//左闭右闭,最左边界
int binarySearch(int[] nums, int target) {
//数组判断
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
//左闭右闭
int right = nums.length - 1;
//注意循环条件
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (num[mid] == target) {
//不直接返回,向左侧边界逼近
right = mid - 1;
} else if (num[mid] < target) {
left = mid + 1;
}
}
//对边界情况进行处理
if (nums[left] != target || left == nums.length) {
return -1;
}
return left;
}
左闭右开
//左闭右闭,最左边界
int binarySearch(int[] nums, int target) {
//数组判断
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
//左闭右开
int right = nums.length;
//注意循环条件
while (left < right){
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid;
} else if (num[mid] == target) {
//不直接返回,向左侧边界逼近
right = mid;
} else if (num[mid] < target) {
left = mid + 1;
}
}
//对边界情况进行处理
if (nums[left] != target || left = nums.length) {
return -1;
}
return left;
}
处理边界情况
边界情况分为两种:
- target比数组中所有数值都大;
- target比数组中所有数值都小。
我们具体来看一下两种边界情况:
target比数组中所有数值都大
左闭右闭
左闭右开
target=7时,返回-1的条件均为left=nums.length
。
target比数组中所有数值都小
左闭右闭
左闭右开
target=-1时,返回-1的条件均为nums[left] != target
。
如果不处理以上两种情况,返回的left下标(前者返回nums.length
,后者返回0)就是错误的。
- 寻找该数在数组中的最右边界
该情况也有两种实现:左闭右闭和左闭右开
左闭右闭
//左闭右闭,最右边界
int binarySearch(int[] nums, int target) {
//数组判断
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
//左闭右开
int right = nums.length - 1;
//注意循环条件
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (num[mid] == target) {
//不直接返回,向右侧边界逼近
left = mid + 1;
} else if (num[mid] < target) {
left = mid + 1;
}
}
//对边界情况进行处理
if (right < 0 || nums[right] != target) {
return -1;
}
return right;
}
注意:代码中的判断条件不可颠倒,否则会发生数组越界
if (right < 0 || nums[right] != target) {
return -1;
}
左闭右开
//左闭右开,最右边界
int binarySearch(int[] nums, int target) {
//数组判断
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
//左闭右开
int right = nums.length;
//注意循环条件
while (left < right){
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid;
} else if (num[mid] == target) {
//不直接返回,向右侧边界逼近
left = mid + 1;
} else if (num[mid] < target) {
left = mid + 1;
}
}
//对边界情况进行处理
if (right == 0 || nums[right - 1] != target) {
return -1;
}
return right - 1;
}
处理边界情况
边界情况分为两种:
- target比数组中所有数值都大;
- target比数组中所有数值都小。
我们具体来看一下两种边界情况:
target比数组中所有数值都大
左闭右闭
target=7时,返回-1的条件为nums[right]!=target
。
左闭右开
target=7时,返回-1的条件为nums[right-1]!=target
。
target比数组中所有数值都小
左闭右闭
target=-1时,返回-1的条件为right < 0
。
左闭右开
target=-1时,返回-1的条件为right = 0
。
注意:两种情况下返回-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,必须减⼀