定义
若从已排序的数组中查找指定的数据,首先查询最中间的数据,如果该数据为指定的目标值,则查找结束。否则根据中间值与目标值的大小差异,从左区间或者右区间继续循环查找,直到查找到指定的目标值,或者循环至待查找区间为空,那么指定的目标值则不存在。
由于每次查找都能排除待查找数据集合区间一半的范围,二分查找也被成为折半查找。
二分查找效率很高,算法时间复杂度为O(logn)
。
关键点
在二分查找中,需要注意如下两个关键点:
1、顺序数组
二分查找只能用于已排序的数组中,只有这样每次查询才能排除掉一半的空间。
2、随机访问
二分查找的高效率是由于每次递归查询时仅仅需要和最中间元素做比较,其依赖数组的随机访问特性。
代码
二分查找可分为递归实现和非递归实现。
递归代码如下:
class binary_search {
public:
int search(vector<int>& nums, int target) {
return search(nums, 0, static_cast<int>(nums.size()) - 1, target);
}
private:
int search(vector<int>& nums, int left, int right, int target) {
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
return search(nums, left, mid - 1, target);
}
if (nums[mid] < target) {
return search(nums, mid + 1, right, target);
}
return mid;
}
};
非递归代码如下:
class binary_search {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = static_cast<int>(nums.size()) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
};
二分查找拓展
在二分查找中,查找目标值是最简单的应用,二分查找存在很多扩展应用,比如说查询某个区间内大于目标值的最小值等,二分查找拓展如下:
upper
返回大于目标值的最小值的索引,如果该值不存在,返回数组的长度值。
由于在[0, nums.size()]
区间范围内必定存在解,不断循环查询后直到left与right相等,表示查询到了结果值的索引。
代码如下:
int upper(const std::vector<T>& nums, const T& target)
{
int left = 0;
int right = static_cast<int>(nums.size());
// 由于区间范围内必定存在结果值的索引,当left与right相等时表示查询到结果,因此循环条件判断语句中不能包含等于条件
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
upper_ceil
如果目标值存在,那么返回最大索引值,如果目标值不存在,返回大于目标值的最小值的索引。
该接口可借助upper
的实现,先调用upper
接口返回大于某个元素的最小值的索引,然后判断该索引减1后该值是否为目标值,如果为目标值,那么该索引为目标值的最大索引值。
代码如下:
int upper_ceil(const std::vector<T>& nums, const T& target)
{
int index = upper(nums, target);
if ((index == 0) || (nums[index - 1] != target)) {
return index;
}
return index - 1;
}
lower_ceil
如果目标值存在,那么返回最小索引值,如果目标值不存在,返回大于目标值的最小值的索引。
lower_ceil
的接口实现和upper
的接口实现类似,不过判断条件有点差异,upper
接口中如果中间值小于等于目标值,那么该中间值及其左边的区间内的值都可以排除掉。而对于lower_ceil
接口,只需要判断中间值小于目标值,那么该中间值及其左边的区间内的值才可以排除掉。代码如下:
int lower_ceil(const std::vector<T>& nums, const T& target)
{
int left = 0;
int right = static_cast<int>(nums.size());
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
lower
返回小于目标值的最大值的索引,如果该值不存在,返回-1
。
由于在[-1, nums.size() - 1]
区间范围内必定存在解,不断循环查询后直到left与right相等,表示查询到了结果值的索引。代码如下:
int lower(const std::vector<T>& nums, const T& target)
{
int left = -1;
int right = static_cast<int>(nums.size()) - 1;
while (left < right) {
// 注意这里的mid取值时需要加1,向右取整,避免无限循环。
int mid = left + (right - left + 1) / 2;
if (nums[mid] < target) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
注意:int mid = left + (right - left + 1) / 2;
,为什么这里右边界减左边界后要加1呢?因为当左右边界相邻时,由于mid = left + 0.5,而0.5会被计算机当成0进行计算,所以mid = left,而如果nums[mid] < target
条件满足,那么left = mid,这样将会陷入无穷循环。为了避免无穷循环,将这里的mid取中间值时向右取整,从而需要在右边界减左边界后做加1操作。而之前,由于int mid = left + (right - left) / 2;
,这个表达式实际上mid取中间值是向左取整,如果满足条件时left = mid + 1;
,所以不会导致无限循环。
lower_floor
如果目标值存在,那么返回最小索引值,如果目标值不存在,返回小于目标值的最大值的索引。
该接口可借助lower
的实现,先调用lower
接口返回小于目标值的最大值的索引,然后判断该索引加1后该值是否为目标值,如果为目标值,那么该索引为目标值的最小索引值。代码如下:
int lower_floor(const std::vector<T>& nums, const T& target)
{
int index = lower(nums, target);
if ((index == static_cast<int>(nums.size())) || (nums[index + 1] != target)) {
return index;
}
return index + 1;
}
upper_floor
如果目标值存在,那么返回最大索引值,如果目标值不存在,返回小于目标值的最大值的索引。
upper_floor
接口实现和lower
的接口实现类似,不过判断条件有点差异,在lower
的接口中,判断条件是如果中间值大于等于目标值,那么将该中间值及其右边的区间内的值都可以排除掉。而对于upper_floor
接口,判断条件是如果中间值大于目标值,那么将该中间值及其右边的区间内的值才可以排除掉。代码如下:
int upper_floor(const std::vector<T>& nums, const T& target)
{
int left = -1;
int right = static_cast<int>(nums.size()) - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (nums[mid] <= target) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}