初学者在学完二分查找后,往往会低估二分查找的功能,他具有更广泛的使用场景,不单单只是寻找其中特定的值,随意一点的改动都会使二分查找的功能发生很大的变化
正如Knuth 大佬(发明 KMP 算法的那位)都说二分查找:
思路很简单,细节是魔鬼。
这篇文章主要带会从二分查找的两种形式实现
到题目训练
加强对二分查找的理解与使用,更重要的是要学会变通
二分查找的两种实现形式
俗话说得好,万变不离其宗
这两种写法就是二分查找的基础,任何变化都离不开这两种写法
这是二分查找的基本框架:
int Search(int[] nums,int len,int target)
{
int left = 0, right = ...;
while(...)
{
int mid = left + (right - left) / 2;
//不直接相加除2是为了防止溢出
if (nums[mid] > target)
right = ...
else if (nums[mid] < target)
left = mid + 1;
else
return mid;
}
return -1;
}
我们发现这个框架中唯一需要变化的地方就是while
主循环与nums[mid]>target
的情况
这也就是二分查找细节与魔鬼的地方
我们还要知道二分查找要查找的话至少能遍历一遍数组,而为了能够遍历这个数组有了常用的两种方法:左闭右开与左闭右闭
左闭右闭
那么什么是左闭右闭呢?
我们看下边这个例子:
设有一个数组{1,2,3,4}
则下标为{0,1,2,3}
则left=0;right=len-1;
那么检索范围的下标是[left,right]
,这也就是我们说的左闭右闭
接下来看需要变化的两个地方:
既然有了检索的下标范围
那我们就可以知道left==right
时是有意义的
主循环就可以写为while(left<=right)
而target < arr[mid]
时,因为target!=arr[mid]
所以right=mid-1
代码实现:
int search(int arr[], int len, int target)
//target是查找的目标值
{
int left = 0;
int right = len - 1;
while (left <= right)
{
int mid = (right - left) / 2 + left;
if (target < arr[mid])
right = mid - 1;
else if (target > arr[mid])
left = mid + 1;
else
return mid;
}
return -1;
}
左闭右开
什么是左闭右开呢?
设有一个数组{1,2,3,4}
则下标为{0,1,2,3}
则left=0;right=len;
那么检索范围的下标是[left,right)
,这也就是我们说的左闭右开
接下来看两个细节的地方:
既然有了检索的下标范围
那我们就可以知道left==right
时是无意义的
主循环就可以写为while(left<right)
而target < arr[mid]
时,因为target!=arr[mid]
,且是左闭右开区间
所以right=mid
,正好不需要重新检查arr[mid]
代码实现:
int search(int arr[], int len, int target)
{
int left = 0;
int right = len ;
while (left < right)
{
int mid = (right - left) / 2 + left;
if (target < arr[mid])
right = mid;
else if (target > arr[mid])
left = mid + 1;
else
return mid;
}
return -1;
}
题目训练
旋转数组的最小数字
牛客链接,链接奉上。
相信大家学习二分查找时都听说过这样一句话,二分查找的前提是有序数组,然而真的是这样吗?
答案显然时否定的,
如果能够明确二分之后,答案存在于二分的某一侧,就可以使用二分。
思路:
我们发现这题没有给出目标值
如果有目标值target,那么直接让arr[mid] 和 target 比较即可。
如果没有目标值,一般可以考虑端点
这里我们考虑右端点
当arr[mid]>target
时,说明mid在最小值左边
当arr[mid]<target
时,说明mid在最小值右边,但不确定mid是不是最小值,所以right=mid
,
注意:这是不是对应着左闭右开区间,所以主循环中是left<rigth
因为会有重复的现象出现,所以重复时right--
代码实现:
int minNumberInRotateArray(int* nums, int numsLen )
{
// write code here二分查找
int right=numsLen-1;
int left=0;
int mid=0;
while(left<right)
{
mid=(left+right)/2;
if(nums[mid]>nums[right])
//利用right是因为利用右界实现比左界便捷,不需要额外的判断
//情况1 :1 2 3 4 5 , nums[mid] = 3. target = 1,
//nums[mid] > target, 答案在mid 的左侧
//情况2 :3 4 5 1 2 , nums[mid] = 5, target = 3,
//nums[mid] > target, 答案却在mid 的右侧
left=mid+1;
else if(nums[mid]<nums[right])
right=mid;
else
right--;
//使用left++时会跳过最小值
//例如当直接为顺序数组时,会直接跳过
}
return nums[left];
}
数字在升序数组中出现的次数(上下界问题)
牛客链接,链接奉上。
思路:
看到这题的第一眼就想到二分查找,因为这题也是寻找一个数,不过是这个数的上下界
但是我们需要做一些调整,
比如寻找左界时,相等要左移,寻找右界时,相等要右移
还要注意特殊情况的判断
代码实现:
int GetNumberOfK(int* data, int dataLen, int k)
{
// write code here
if (dataLen == 0)
//特殊情况,数组长度为0
return 0;
else if (data[dataLen - 1]<k || data[0]>k)
//特殊情况,k小于或大于最小值或最大值
return 0;
else
{
int leftbound, rightbound;
int mid;
int left = 0, right = dataLen - 1;
if (left == right)
return k == data[left];
else
{
//寻找左界
while (left <= right)
{
//可以利用第一种情况,左闭右闭
//第二种也可以,但是要改主循环条件为left<right
//right要=mid
mid = (left + right) / 2;
if (data[mid] > k)
right = mid - 1;
else if (data[mid] < k)
left = mid + 1;
else
right--;
}
leftbound = left;
//寻找右界
left = 0, right = dataLen - 1;
while (left <= right)
{
mid = (left + right) / 2;
if (data[mid] > k)
right = mid - 1;
else if (data[mid] < k)
left = mid + 1;
else
left++;
rightbound = right;
}
return rightbound - leftbound + 1;
}
}
}
有错的话欢迎纠错,绝对改正