二分查找总结
二分查找总结
1. 二分查找的工作方式
二分查找维护查找空间的左、右和中间指示符,并比较查找目标或将查找条件应用于集合的中间值;如果条件不满足或值不相等,则清除目标不可能存在的那一半,并在剩下的一半上继续查找,直到成功为止。
如果查询空间已经空了,仍然无法满足查找条件,则无法找到目标。
一般而言,二分查找是对 有序且无重复的序列
进行查找,但某些时候局部有序或序列有重复元素时也可进行二分查找。
二分查找的三个步骤:
- 预处理 —— 如果集合未排序,则进行排序。
- 二分查找 —— 使用循环或递归在每次比较后将查找空间划分为两半。
- 后处理 —— 在剩余空间中确定可行的候选者。
2. 二分查找的两种写法
第一种写法是定义 target
在一个左闭右闭的区间 [left, right]
内,在这种写法中,有以下几点需要注意:
- 初始化:初始化时
left = 0, right = nums.size() - 1
; - 循环判断条件:
while(left <= right)
,注意这里是<=的, 因为left == right
是有意义的,此时区间只含有一个元素 - 查找区间更新:
if(nums[mid] < target)
时,left = mid + 1
;if(nums[mid] > target)
时,right = mid - 1
,这里 right 是更新为 mid - 1的。 - 循环结束时, 区间边界关系满足:
left = right + 1;
// 左闭右闭写法
int binary_search(vector<int>& nums, int target)
{
int left = 0, right = 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;
}
第二种写法是定义 target 在一个左闭右开的区间 [left, right)
内,在这种写法中,有以下几点需要注意:
- 初始化:初始化时,
left = 0, right = nums.size()
,这里 right 是取 nums.size()的,因为 right 处是开区间取不到的; - 循环判断条件:
while(left < right)
,注意这里是<的,因为区间是[left, right)
,left == right是无意义的;
- 查找区间更新:
if(nums[mid] < target)
时,left = mid + 1
;if(nums[mid] > target)
时,right = mid
,这里 right 是更新为 mid 的,因为 mid 处的值显然不是 target,同时 right 是取不到的,下次查找时也不会包括nums[mid]
; - 循环结束时,区间边界关系满足:
left == right
;
// 左闭右开写法
int binary_search(vector<int>& nums, int target)
{
int left = 0, right = nums.size();
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] > target)
right = mid;
else if (nums[mid] < target)
left = mid + 1;
else
return mid;
}
return -1;
}
3. 二分查找目标值和中间值的比较
对于 if(nums[mid] < target)
这一中间值与目标值的比较关系来说,有以下结论:
- 若是左闭右闭区间写法, 在查找结束后,left 一定指向
第一个大于等于
目标值的元素的位置,同时 right 一定指向最后一个小于
目标值的位置; - 若是左闭右开区间写法,在查找结束后,left 和 right 都指向
第一个大于等于目标值
的元素的位置;
对于if(nums[mid] <= target)
这一中间值与目标值的比较关系来说,有以下结论:
- 若是左闭右闭区间写法, 在查找结束后,left一定指向
第一个大于目标值
的元素的位置,同时right一定指向最后一个小于等于
目标值的位置; - 若是左闭右开区间写法,在查找结束后,left 和 right 都指向
第一个大于目标值
的元素的位置;
4. 二分查找模板
模板一
- 初始化:
int left = 0, right = nums.size() - 1
; - 循环条件:
left <= right
; - 左右边界更新:
left = mid + 1
;right = mid - 1
; - 循环终止:
left > right
; - 两种情形:
if(nums[mid] <= target)
left = mid + 1;
else
right = mid - 1;
>target
的数组范围是 nums[left]至 nums 数组末尾
<= target
的数组范围是 nums[0]至 nums[right]
if(nums[mid] < target)
left = mid + 1;
else
right = mid - 1;
>=target
的数组范围是 nums[left]至 nums 数组末尾
<target
的数组范围是 nums[0] 至 nums[right]
- 模板一经典代码:
int binarySearch(vector<int>& nums, int target)
{
int left = 0, right = nums.size() - 1;
// 定义target在闭区间[left, right]中
while (left <= right)
{
int mid = left + (right - left) / 2;
if (nums[mid] > target)
right = mid - 1; // target在左区间[left, mid - 1]
else if (nums[mid] < target)
left = mid + 1; // target在右区间[mid + 1, right]
else
return mid;
}
return -1;
}
模板二
- 初始化:
int left = 0, right = nums.size()
; - 循环条件:
left < right
; - 左右边界更新:
left = mid + 1
;right = mid
; - 循环终止:
left == right
;
模板二适用于查找需要访问数组中当前索引及其直接右邻居索引的元素或条件
;该模板保证查找空间在每一步中至少有 2 个元素;但这种模板需要进行后处理,当查找空间只剩下 1 个元素时,循环或递归结束,此时需要评估剩余的这个元素是否符合条件。
- 模板二经典代码:
int binarySearch(vector<int>& nums, int target)
{
int left = 0, right = nums.size();
// 定义target在左闭右开区间[left, right]中
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] > target)
right = mid; // target在左区间[left, mid)中
else if (nums[mid] < target)
left = mid + 1; // target在有区间[mid + 1, right)中
else
return mid;
}
// 后处理
if(left != nums.size() && nums[left] == target) return left;
return -1;
}
- 查找
第一个满足xxx条件
的元素的位置
// 查找第一个满足xxx条件的元素的位置
int left = 0, right = nums.size() - 1;
while (left < right) // 对于区间[left, right]来说,left == right意味着找到了唯一位置
{
int mid = left + (right - left) / 2;
if (满足XXX条件)
right = mid; //条件成立,则往左区间[left, mid]查找
else
left = mid + 1; //条件不成立,则往右区间[mid + 1, right]查找
}
return left; // 查找到最后, left == right
- 查找
满足xxx条件的最后一个元素
的位置
// 查找最后一个满足xxx条件的元素的位置
int left = 0, right = nums.size() - 1;
while (left < right) // 对于区间[left, right]来说,left == right意味着找到了唯一位置
{
int mid = left + (right - left) / 2;
if (不满足XXX条件) //注意是不满足!
right = mid; //条件成立,则往左区间[left, mid]查找
else
left = mid + 1; //条件不成立,则往右区间[mid + 1, right]查找
}
return left - 1; // 查找到最后, left == right, left是第一个不满足xxx条件的,因此left - 1就是最后一个满足xxx条件的位置
- 查找
满足 xxx 条件的最后一个元素
的位置
// 查找最后一个满足xxx条件的元素的位置
int left = 0, right = nums.size() - 1;
while (left < right)
{
int mid = left + (right - left + 1) / 2; // +1是为了让相除结果向上取整
if (满足XXX条件)
left = mid;
else
right = mid - 1;
}
return left;
不过这几个模板得到的结果都需要后处理判断:
- 首先可能出现索引越界问题,因此需要判断索引是否合法!
例如:return left >=0 && left <= nums.size() - 1 ? left : -1;
- 其次,若问题不一定保证能查找到答案,那么就要判断得到的是否是正确答案!
例如:return nums[left] == target ? left : -1;
模板三
- 初始化:
int left = 0, right = nums.size() - 1
; - 循环条件:
left + 1 < right
; - 左右边界更新:
left = mid
;right = mid
; - 循环终止:
left + 1 == right
;
模板三适用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件
;使用元素的邻居来确定它是向右还是向左查找;保证查找空间在每个步骤中至少有 3 个元素
,当查找空间只剩下 2 个元素时,循环或递归结束,此时需要进行后处理,判断剩余的两个元素是否符合条件。
- 模板三经典代码
int binarySearch(vector<int>& nums, int target)
{
if (nums.size() == 0)
return -1;
int left = 0, right = nums.size() - 1;
// 定义target在开区间(left, right)中
while (left + 1 < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid; // target在右区间(mid, right)中
else
right = mid; // target在左区间(left, mid)中
}
// 后处理:
if(nums[left] == target) return left;
if(nums[right] == target) return right;
return -1;
}
- 三种模板的比较