赞
踩
作者:来知晓
公众号:来知晓
刷题交流QQ群:444172041
update:2021年11月9日
二分法常用在数组和查找场景,可以用logN
的时间复杂度,查找到某值或者上下区间,效率较高,使用频繁,但又经常容易出错,故此总结用法供后面查阅回顾。
对边界值和不等情况时的下标移动,务必要理解透彻,并多练习检验。如果不想关注具体代码实现细节,可以直接拉到末尾,看二分对比总结表格,一目了然。
常见区间条件:
[left, right)
,跳出条件一定为left == right
[left, right]
,跳出条件一定为left > right
下文中未明确说明的,使用的区间统一为左闭右开:[left, right)
。
自写代码版本功能:
代码实现如下:
// 输入:升序数组,数组大小及待查找的目标 // 输出:若查找到,则返回对应下标;否则返回-1 int binarySearch(int *nums, int numsSize, int target) { int left = 0; int right = numsSize; int mid; while (left < right) { 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; // 没找到,则返回-1 }
库函数版本功能:
函数签名:void *bsearch(const void *_Key, const void *_Base, size_t _NumOfElements, size_t _SizeOfElements, int (*_PtFuncCompare)(const void *, const void *) __attribute__((cdecl)))
头文件:#include <stdlib.h>
重要说明:
调用demo代码如下:
#include <stdlib.h> int Compar(void *a, void *b) { return *(int *)a - *(int *)b; } // 找有序数组的值 int main(void) { int nums[10] = {0, 0, 2, 2, 4, 5, 6, 9, 9, 9}; int numsSize = 10; int key = 6; int *ret = (int*)bsearch(&key, nums, numsSize, sizeof(int), Compar); if (ret != NULL) { printf("target == %d, index = %d\n", *ret, (int)(ret - &nums[0])); // 获得下标 } else { printf("target not found\n"); } return 0; }
文中左右边界指的是数组连续重复值的左右侧,为闭区间。如 int nums[10] = {0, 0, 2, 2, 4, 5, 6, 9, 9, 9};
,数组中目标值为9的范围是[7, 9]
,即左侧边界下标为7, 右侧边界下标为9。
上一个版本的增强版,可以针对重复值,找到数组目标值出现的左边界。
// 输入:升序数组,数组大小及待查找的目标,存在重复值 // 输出:若查找到,则返回出现该值的左侧边界对应下标;否则返回-1 int binarySearchLowerBound(int *nums, int numsSize, int target) { int left = 0; int right = numsSize; int mid; int flag = 0; // 默认没找到 while (left < right) { mid = left + (right - left) / 2; if (nums[mid] == target) { right = mid; // 若找到,缩小右区间 flag = 1; } else if (nums[mid] < target) { // 左侧区间右移 left = mid + 1; } else if (nums[mid] > target) { // 右侧区间左移 right = mid; } } if (flag == 1) { // 找到 return left; // right = mid,left到达mid时跳出 } return -1; // 没找到,则返回-1 }
代码实现如下:
// 输入:升序数组,数组大小及待查找的目标,存在重复值 // 输出:若查找到,则返回出现该值的右侧边界对应下标;否则返回-1 int binarySearchUpperBound(int *nums, int numsSize, int target) { int left = 0; int right = numsSize; int mid; int flag = 0; // 默认没找到 while (left < right) { mid = left + (right - left) / 2; if (nums[mid] == target) { left = mid + 1; // 若找到,缩小左区间 flag = 1; } else if (nums[mid] < target) { // 左侧区间右移 left = mid + 1; } else if (nums[mid] > target) { right = mid; // 注意 } } if (flag == 1) { // 找到 return left - 1; // 因mid相等时,left = mid + 1 } return -1; // 没找到,则返回-1 }
查找左右边界的控制,靠bound入参决定,如bound = 0
,则找左边界;如bound = 1
,则找右边界。
返回值分析,找左边界,相等时不断缩小右边界,到左侧相等才跳出,所以返回left
。找右边界,相等时不断缩小左边界,到右侧相等才跳出,右侧相等时的right
是开区间取不到的,所以是right - 1
。
另外可以发现,该函数稍作更改,可以实现查找在数组中重复出现的某值区间范围的功能,且输出范围为左闭右开,只需要把bound = 1
时,返回值right - 1
,改为right
即可,也即返回第一个大于目标值的下标。
代码实现如下:
// 输入:升序数组,数组大小及待查找的目标,存在重复值 // 输出:若查找到,则返回出现该值的右侧边界对应下标;否则返回-1 int binarySearchBound(int *nums, int numsSize, int target, int bound) { int left = 0; int right = numsSize; while (left < right) { int mid = left + (right - left) / 2; int condition = (bound == 0) ? (nums[mid] < target) : (nums[mid] <= target); // 合并== if (condition) { left = mid + 1; // 左侧区间更新 } else { // 右侧区间更新 right = mid; } } if (bound == 0 && nums[left] == target) { // 若是左侧边界 return left; } if (bound == 1 && nums[right - 1] == target) { // 若是右侧边界 return right - 1; // = left - 1 } return -1; }
需要注意的是,不存在target的时候,直接返回-1。在二分查找值时,返回条件是nums[mid] == target
时直接return,而查找左右侧边界时,返回条件则需要等while()
循环完毕后,才能返回。观察下表可知,区间右侧开闭主要影响right的更新和while判断。
场景 | 左闭右开[left, right) | 左闭右闭[left, right] | 备注 |
---|---|---|---|
初始赋值 | left = 0, right = numsSize | left = 0, right = numsSize - 1 | 部分不同 |
while条件 | left < right | left <= right | 不同 |
nums[mid] < target | left = mid + 1 | left = mid + 1 | 相同 |
nums[mid] > target | right = mid | right = mid - 1 | 不同 |
nums[mid] == target | 返回mid | 返回mid | 相同 |
下面左右侧边界查找采用的是左闭右开区间,读者有兴趣可自行分析左闭右闭区间对应的情况。注意,如果有左边界不存在的场景,在while循环后,要判断下标对应值是否与target相等。
观察下表可知,在区间开闭情况相同时,左右侧边界的查找的主要区别在于nums[mid] == target
时边界更新和返回值。
场景 | 左侧边界 | 右侧边界 | 备注 |
---|---|---|---|
初始赋值 | left = 0, right = numsSize | left = 0, right = numsSize | 相同 |
while条件 | left < right | left < right | 相同 |
nums[mid] < target | left = mid + 1 | left = mid + 1 | 相同 |
nums[mid] > target | right = mid | right = mid | 相同 |
nums[mid] == target | right = mid | left = mid + 1 | 不同 |
返回值 | left | left - 1 | 不同 |
通过实战练习并能自己独立实现,方能检验是否理解到位,推荐以下练习:
附闭区间二分查找代码实现供参考:
int search(int* nums, int numsSize, int target){ if (nums == NULL) return -1; int left = 0; int right = numsSize - 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; // 遍历完未找到 }
相关LeetCode习题:
找值
找左边界
找右边界
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。