二分查找(Binary Search)算法,也叫折半查找算法。二分查找针对的时一个有序的数据集合,查找思想有点类似于分治。每次都是通过和区间的中间元素进行比较,将待查区缩小为原来的一半,直到将元素找到或者区间缩小为0。
我们可以通过2种方式实现:递归和非递归。
/*************************************************************************
> File Name: bsearch.c
> Author: jinshaohui
> Mail: [email protected]
> Time: 18-10-21
> Desc:
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int mybsearch(int a[],int size,int value)
{
int mid = 0;
int left = 0;
int right = size - 1;
while(left <= right)
{
/*防止size数量太大是,(left + right)数据翻转,导致问题*/
mid = left + ((right - left)>>1);
if (a[mid] == value)
{
return mid;
}
else if (a[mid] < value)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
return -1;
}
int helper(int a[], int left,int right,int value)
{
int mid = 0;
if (left > right)
{
return -1;
}
/*防止size数量太大是,(left + right)数据翻转,导致问题*/
mid = left + ((right - left)>>1);
if (a[mid] == value)
{
return mid;
}
else if (a[mid] < value)
{
return helper(a,mid + 1,right,value);
}
else
{
return helper(a,left,mid - 1,value);
}
return -1;
}
/*递归实现*/
int mybsearch_2(int a[],int size,int value)
{
return helper(a,0,size-1,value);
}
int main()
{
int a[10] = {5,6,8,9,10,11,23,42,53,123};
int data = 0;
int res = 0;
printf("\r\n输入一个整数");
scanf("%d",&data);
res = mybsearch(a,10,data);
printf("data[%d] %s 在数据中,下标是%d",data,(res != -1)?"":"不",res);
printf("\r\n输入一个整数");
scanf("%d",&data);
res = mybsearch_2(a,10,data);
printf("data[%d] %s 在数据中,下标是%d",data,(res != -1)?"":"不",res);
return;
}
看了上面的代码,我们觉得很容易吧。但是还有几个地方需要注意:
1、循环退出条件
这里必须是left <= right,而不是left < right
2、mid的求值
还有一个是为了程序的更好的健壮性:求中间值的时候,我们多用mid=(left+right)/2; 但是这样的可能会越界,比如 left+right超int型的范围,所以我们用更好更安全的写法 :mid=left+(right-left)/2; 起初还钻牛角尖不理解为什么等式相同。。。 为了进一步将性能优化到极致的话,我们采用位移操作。>>1 位移优先级比较低,我们必须考虑到加括号。mid=left+((right-left) >>1).
3、left和right的更新
left = mid + 1;right = mid - 1;这里必须是+1 和-1,不能直接等于mid,否则可能会导致死循环。
二分查找的变形问题
我们一直以为二分查找比较简单,但是写出完全正确的二分查找算法确很难。首先引用一下《编程珠玑》中的两句话:
1、尽管给了那么充裕的时间,只有大约10%的专业程序员能够写出正确的二分查找。
2、尽管第一个二分查找程序于1946年就公布了,但是第一个没有bug的二分查找程序在1962年才出现。
二分查找的变形问题很多,我们重点来看下面四种
1、找出第一个等于给定数值的元素
2、找出最后一个等于给定数值的元素
3、找出第一个大于等于给定数值的元素
4、找出第一个小于等于给定数值的元素。
/*************************************************************************
> File Name: bsearch.c
> Author: jinshaohui
> Mail: [email protected]
> Time: 18-10-21
> Desc:
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
/*二分查找算法的变形问题
*1、查找第一个等于给定数值的元素
*2、查找最后一个等于给定数值的元素
*3、查找第一个大于等于给定数值的元素
*4、查找第一个小于等于给定数值的元素
* */
/*1、查找第一个等于给定数值的元素*/
int mybsearch_1(int a[],int size,int value)
{
int mid = 0;
int left = 0;
int right = size - 1;
while(left <= right)
{
/*防止size数量太大是,(left + right)数据翻转,导致问题*/
mid = left + ((right - left)>>1);
if (a[mid] < value)
{
left = mid + 1;
}
else if (a[mid] > value)
{
right = mid - 1;
}
else
{
if ((mid == 0) || (a[mid - 1] != value))
{
return mid;
}
else
{
right = mid - 1;
}
}
}
return -1;
}
/*2、查找最后一个等于给定数值的元素*/
int mybsearch_2(int a[],int size,int value)
{
int mid = 0;
int left = 0;
int right = size - 1;
while(left <= right)
{
/*防止size数量太大是,(left + right)数据翻转,导致问题*/
mid = left + ((right - left)>>1);
if (a[mid] < value)
{
left = mid + 1;
}
else if (a[mid] > value)
{
right = mid - 1;
}
else
{
if ((mid == (size - 1)) || (a[mid + 1] != value))
{
return mid;
}
else
{
left = mid + 1;
}
}
}
return -1;
}
/*3、查找第一个大于等于给定数值的元素*/
int mybsearch_3(int a[],int size,int value)
{
int mid = 0;
int left = 0;
int right = size - 1;
while(left <= right)
{
/*防止size数量太大是,(left + right)数据翻转,导致问题*/
mid = left + ((right - left)>>1);
if (a[mid] < value)
{
left = mid + 1;
}
else
{
/*a[mid] >= value 当mid==0 或者a[mid-1] > value 说明是第一个大于等于value*/
if ((mid == 0) || (a[mid - 1] < value))
{
return mid;
}
else
{
right = mid - 1;
}
}
}
return -1;
}
/*4、查找第一个小于等于给定数值的元素*/
int mybsearch_4(int a[],int size,int value)
{
int mid = 0;
int left = 0;
int right = size - 1;
while(left <= right)
{
/*防止size数量太大是,(left + right)数据翻转,导致问题*/
mid = left + ((right - left)>>1);
if (a[mid] > value)
{
right = mid - 1;
}
else
{
/*a[mid] <= value 时,当前mid == size -1 数组中最大的数值;
* 或者a[mid + 1] 大于vlaue,就是mid就第一个小于等于value*/
if ((mid == (size - 1)) || (a[mid + 1] > value))
{
return mid;
}
else
{
left = mid + 1;
}
}
}
return -1;
}
int main()
{
int a[10] = {5,6,6,9,10,11,11,22,33,33};
int data = 0;
int i = 0;
int res =0;
printf("\r\n");
for(i = 0; i < 10 ; i++)
{
printf("%d ",a[i]);
}
printf("\r\n");
printf("\r\n输入一个整数");
scanf("%d",&data);
res = mybsearch_1(a,10,data);
printf("第一个等于data[%d],下标是%d",data,res);
printf("\r\n输入一个整数");
scanf("%d",&data);
res = mybsearch_2(a,10,data);
printf("最后一个等于data[%d],下标是%d",data,res);
printf("\r\n输入一个整数");
scanf("%d",&data);
res = mybsearch_2(a,10,data);
printf("第一个大于等于data[%d],下标是%d",data,res);
printf("\r\n输入一个整数");
scanf("%d",&data);
res = mybsearch_2(a,10,data);
printf("第一个小等于data[%d],下标是%d",data,res);
return;
}
课后思考:
1、如何求一个数的平方根
/*************************************************************************
> File Name: sqrt.c
> Author: jinshaohui
> Mail: [email protected]
> Time: 18-10-31
> Desc:
************************************************************************/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
/*求解精度设置*/
#define E 0.000001
double mybsearch(double num)
{
double start = 1.0;
double end = num;
double mid = 0.0;
while(1)
{
mid = (start + end)/2;
if(((mid*mid - num) <= E) && ((mid*mid - num) >= -E))
{
return mid;
}
if ((mid*mid - num) > E)
{
end = mid;
}
else
{
start = mid;
}
}
return 0;
}
int main()
{
double num = 0.0;
/*这里需要注意:double的输入方式*/
scanf("%lf",&num);
printf("\r\n num %lf的平方根是%lf",num,mybsearch(num));
return 0;
}
2、如果使用链表存储数据,二分查找的时间复杂度怎么计算?
假设链表长度为n,二分查找每次都要找到中间点(计算中忽略奇偶数差异):
第一次查找中间点,需要移动指针n/2次;
第二次,需要移动指针n/4次;
第三次需要移动指针n/8次;
…
以此类推,一直到1次为值
总共指针移动次数(查找次数) = n/2 + n/4 + n/8 + …+ 1,这显然是个等比数列,根据等比数列求和公式:Sum = 2n - 1.
最后算法时间复杂度是:O(2n-1),忽略常数,记为O(n),
3、快速定位IP地址对应的省份
当我们要查找202.102.133.13对应的IP地址的省份的时候,发现IP地址在[202.102.133.0, 202.102.133.255] 这个范围就返回山东东营市。那我们如何在庞大的地址库中找到指定IP的范围呢?
[202.102.133.0, 202.102.133.255] 山东东营市
[202.102.135.0, 202.102.136.255] 山东烟台
[202.102.156.34, 202.102.157.255] 山东青岛
[202.102.48.0, 202.102.48.255] 江苏宿迁
[202.102.49.15, 202.102.51.251] 江苏泰州
[202.102.56.0, 202.102.56.255] 江苏连云港
我们可以运用上面的第3或第四来通过二分查找来在有序集合中找到。
我们可以将ip地址转换成一个int型:202.102.133.13 = 202256+102256+133256+13256,然后在地址库中找到最后一个小于等于这个地址的数值,他对应的就是我们要查找的省份。