二分查找
二分查找是基于有序序列的查找算法。令[left,right]为整个序列的下标区间,然后每次测试当前[left,right]的中间位置mid=(left+right)/2,判断A[mid]与欲查询的元素x的大小。
- 如果A[mid]==x,说明查找成功,退出查询
- 如果A[mid]>x,说明元素x在mid位置的左边,因此往左子区间[left,mid-1]继续查找
- 如果A[mid]<x,说明元素x在mid位置的右边,因此往右子区间[mid+1,right]继续查找
例题
从序列A={3,7,8,11,15,21,33,52,66,88}中查询数字11和34的位置,其中序列下标为1到10。
先查询11,令left=1、right=10,表示当前查询的下标范围。
- [left,right]=[1,10],因此下标中点mid=(left+right)/2=5。由于A[mid]=A[5]=15,15>11,说明需要在[left,mid-1]范围内继续查找,因此令right=mid-1=4。
- [left,right]=[1,4],因此下标中点mid=(left+right)/2=2。由于A[mid]=A[2]=7,而7<11,说明需要在[mid+1,right]范围内继续查找,因此令left=mid+1=3。
- [left,right]=[3,4],因此下标中点mid=(left+right)/2=3。由于A[mid]=A[3]=8,而8<11,说明需要在[mid+1,right]范围内继续查找,因此令left=mid+1=4。
- [left,right]=[4,4],因此下标中点mid=(left+right)/2=4。由于A[mid]=A[4]=11,而11==11,说明找到了欲查询的数字,因此结束算法,返回下标4。
查询34,令left=1、right=10 - [left,right]=[1,10],因此下标中点mid=(left+right)/2=5。由于A[mid]=A[5]=15,而15<34,说明需要在[mid+1,right]范围内继续查找,因此令left=mid+1=6
- [left,right]=[6,10],因此下标中点mid=(left+right)/2=8。由于A[mid]=A[8]=52,而52>34,说明需要在[left,mid-1]范围内继续查找,因此令right=mid-1=7
- [left,right]=[6,7],因此下标中点mid=(left+right)/2=6。由于A[mid]=A[6]=21,而21<34,说明需要在[mid+1,right]范围内继续查找,因此令left=mid+1=7
- [left,right]=[7,7],因此下标中点mid=(left+right)/2=7。由于A[mid]=A[7]=33,而33<34,说明需要在[mid+1,right]范围内继续查找,因此令left=mid+1=8
- [left,right]=[8,7],由于left>right,因此查找失败,说明序列中不存在34
二分查找的过程与序列的下标从0开始还是从1开始无关。
#include <stdio.h>
//二分区间为左闭右闭的[left,right],传入的初值为[0,n-1]
int binarySearch(int A[], int left, int right, int x)
{
int mid; //mid为left和right的中点
while(left <= right) //如果left>right说没办法形成闭区间
{
mid = (left + right)/2; //取中点
if(A[mid] == x)
return mid; //找到x,返回下标
else if(A[mid] > x) //中间的数大于x
{
right = mid - 1; //往左子区间[left,mid-1]查找
}
else //中间的数小于x
{
left = mid + 1; //往右子区间[mid+1,right]查找
}
}
return -1; //查找失败,返回-1
}
int main()
{
const int n = 10;
int A[n] = {1, 3, 4, 6, 7, 8, 10, 11, 12, 15};
printf("%d %d\n", binarySearch(A, 0, n-1, 6), binarySearch(A, 0, n-1, 9));
return 0;
}
3 -1
求序列中第一个大于等于x的元素位置
假设当前的二分区间为左闭右闭区间[left,right],那么可以根据mid位置处的元素与欲查询元素x的大小来判断应当往哪个子区间继续查找:
- 如果A[mid]>=x,说明第一个大于等于x的元素的位置一定在mid处或者mid左侧,应往左子区间[left,mid]继续查询,即令right=mid
- 如果A[mid]<x,说明第一个大于等于x的元素的位置一定在mid的右侧,应往右子区间[mid+1,right]继续查询,即令left=mid+1
//A[]为递增序列,x为欲查询的数,函数返回第一个大于等于x的元素的位置
//二分上下界为左闭右闭的[left,right],传入的初值为[0,n]
int lower_bound(int A[], int left, int right, int x)
{
int mid; //mid为left和right的中点
while(left < right) //对[left,right]来说,left==right意味着找到唯一位置
{
mid = (left + right) / 2; //取中点
if(A[mid >= x) //中间的数大于等于x
{
right = mid; //往左子区间[left, mid]查找
}
else //中间的数小于x
{
left = mid + 1; //往右子区间[mid+1, right]查找
}
}
return left; //返回夹出来的位置
}
需要注意:
- 循环条件为left<right而非之前的left<=right
- 由于当left==right时while循环停止,因此最后的返回值既可以时left,也可以是right
- 二分的初始区间应当能覆盖到所有可能返回的结果
求序列中第一个大于x的元素的位置
假设当前区间为[left,right],那么可以根据mid位置处的元素与欲查询元素x的大小来判断应当往哪个子区间继续查找:
- 如果A[mid]>x,说明第一个大于x的元素的位置一定在mid处或mid的左侧,应往左子区间[left, mid]继续查询
- 如果A[mid]<=x,说明第一个大于x的元素的位置一定在mid的右侧,应往右子区间[mid+1,right]继续查询
//A[]为递增序列,x为欲查询的数,函数返回第一个大于x的元素的位置
//二分上下界为左闭右闭的[left,right],传入的初值为[0,n]
int upper_bound(int A[], int left, int right, int x)
{
int mid; //mid为left和right的中点
while(left < right) //对[left,right]来说,left==right意味着找到唯一位置
{
mid = (left + right) / 2; //取中点
if(A[mid] > x) //中间的数大于x
{
right = mid; //往左子区间[left,mid]查找
}
else //中间的数小于等于x
{
left = mid + 1; //往右子区间[mid+1, right]查找
}
}
return left; //返回夹出来的位置
}
模板
寻找有序列中第一个满足某条件的元素的位置
- 左闭右闭
//解决“寻找有序序列第一个满足某条件的元素的位置”问题的固定模板
//二分区间为左闭右闭的[left,right],初值必须能覆盖解的所有可能取值
int solve(int left, int right)
{
int mid; //mid为left和right的中点
while(left < right) //对[left,right]来说,left==right意味着找到唯一位置
{
mid = (left + right) / 2; //取中点
if(条件成立) //条件成立,第一个满足条件的元素的位置<=mid
{
right = mid; //往左子区间[left,mid]查找
}
else //条件不成立,则第一个满足该条件的元素的位置>mid
{
left = mid + 1; //往右子区间[mid+1,right]查找
}
}
return left; //返回夹出来的位置
}
- 左开右闭
//解决“寻找有序序列第一个满足某条件的元素的位置”问题的固定模板
//二分区间为左开右闭的(left,right]
//初值必须能覆盖解的所有可能取值,并且left比最小取值小1
int solve(int left, int right)
{
int mid; //mid为left和right的中点
while(left + 1 < right) //对(left,right]来说,left+1==right意味着找到唯一位置
{
mid = (left + right) / 2; //取中点
if(条件成立) //条件成立,第一个满足条件的元素的位置<=mid
{
right = mid; //往左子区间[left,mid]查找
}
else //条件不成立,则第一个满足该条件的元素的位置>mid
{
left = mid; //往右子区间[mid,right]查找
}
}
return right; //返回夹出来的位置
}