二分查找法(Binary Search)(折半查找)
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。它充分利用了元素间的次序关系,采用分治策略(分半),可在最坏的情况下用O(log2n)完成搜索任务。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
使用条件
折半查找法使用的限制条件为:所要查找的静态范围,如数组,必须是顺序排列的。
例如:无序数组:arr1 = {1,4,3,5,2,6,8,7,9};
要想在数组arr1中用折半查找法(二分查找法)找出关键字8,首先第一步要对数组arr1排序(从小到大,或从大到小都可)。对数组从小到大排序,存为arr2 = {1,2,3,4,5,6,7,8,9};
,这样才可以实现用折半查找法。
可以运用 C语言 冒泡排序法bubble sort 对数组进行排序。
算法思想
折半查找(Binary Search)法核心思想是:逐步缩小范围;
折半查找的核心查找过程是:先确定需要查找关键值所在范围(区间),然后逐步缩小范围直到找到或找不到该记录为止。
以数组arr1举例。
查找思想
1、找到数组的中间位置
2 、检测中间位置的数据是否与要查找的数据key相等
a: 相等,找到,打印下标,跳出循环
b: key < arr[mid], 则key可能在arr[mid]的左半侧,继续到左半侧进行二分查找
c: key > arr[mid], 则key可能在arr[mid]的右半侧,继续到右半侧进行二分查找
如果找到返回下标,否则继续,直到区间中没有元素时,说明key不在集合中,打印找不到
查找过程
①:确定要查找的关键字key = 8。
②:首先对arr1进行排序。得到数组{1,2,3,4,5,6,7,8,9}。
③:设置数组的下限arr2[0] = 1;为最小值,将它定义为:left = arr[0];上限arr2[8] = 9;为最大值,将它定义为:right = arr[8];中间值为数组中间下标的值,mid = (left+ right)/ 2。
④:将mid位置的值与关键值key比较,
最为理想情况下,若arr2[mid] = key,则一次性找到了关键字key;
若arr2[mid] > key,说明数组中间位置的值比关键字大,则需要在数组的左侧上半段寻找;
若arr2[mid] < key,说明数组中间位置的值比关键字小,则需要在数组的右侧下半段寻找;
以此循环直到找到数组中的key值或者left > right为止。
如果在数组中找到了关键字key,则输出打印数组的下标。
如果left > right时还没有找到,则说明数组中没有要找的关键值。
查找过程图示如下:
代码实例
折半查找法一共有两种方法的代码:
方法一,采用[left, right] 前闭后闭区间:
// 方法一,采用[left, right] 区间
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9};
int key = 8;
int left = 0;
int right = sizeof(arr)/sizeof(arr[0])-1; // right位置的数据可以取到
while(left<=right) // right位置有数据,必须要添加=号
{
int mid = left+(right-left)/2;
if(arr[mid]>key) // key小于中间位置数据,说明key可能在左半侧,需要改变右边界
{
right = mid-1; // right位置的数据可以取到,因此right=mid-1
}
else if(arr[mid]<key)// key大于中间位置数据,说明key可能在右半侧,需要改变左边界
{
left = mid+1; // left位置的数据可以取到,因此left=mid+1
}
else
{
printf("找到了,下标是:%d\n", mid);
break;
}
}
if(left>right)
printf("找不到\n");
return 0;
}
// 方法二,采用[left, right) 前闭后开区间:
// 方法二,采用[left, right) 区间
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9};
int key = 8;
int left = 0;
int right = sizeof(arr)/sizeof(arr[0]); // right位置的数据取不到
while(left<right) // right位置没有数据,此处不需要添加=
{
int mid = left+(right-left)/2;
if(arr[mid]>key) // key小于中间位置数据,说明key可能在左半侧,需要改变右边界
{
right = mid; // right位置的数据取不到,因此right=mid,不需要减1
}
else if(arr[mid]<key)// key大于中间位置数据,说明key可能在右半侧,需要改变左边界
{
left = mid+1; // left位置的数据可以取到,因此left=mid+1
}
else
{
printf("找到了,下标是:%d\n", mid);
break;
}
}
if(left>=right)
printf("找不到\n");
return 0;
}
输出:
找到了,下标是:7
请按任意键继续. . .
在这两种方法中有很多易错点和易混淆点
1. right的右半侧区间取值,该值决定了后序的写法
2. while循环的条件是否有等号
3. 求中间位置的方法,直接相加除2容易造成溢出
4. 更改left和right的边界时,不确定是否要+1和-1
过程分析
折半查找的运行过程可以用二叉树来表示,这棵树通常称为“判定树”。例如arr2中的静态查找集合数组中做折半查找的过程,对应的判定树如图所示 :
从图中我们可以看出:如果想找到key值8的话,就要在数组中进行3次查找;第一次找到5,经过比较,发现key值应该在5的右半侧值为6~9的范围内;第二次找到7,经过比较,发现key值应该在7的右半侧值为8或9内,第三次找到key值8。
可以发现,对于具有 n 个结点(查找集合中有n个元素)的判定树,它的层次数至多为:log2n + 1(如果结果不是整数,则做向上取整操作,例如:log29 = 3 +1 = 4 )。
算法复杂度
最好情况下,复杂度为1;
最坏情况下,复杂度为O(log2n)(具体计算时向上取整)。
折半查找法的优缺点
优点:比较次数少,查找速度快,平均性能好;
缺点:是要求待查集合为有序集合,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序集合。
当查找表使用链式存储结构表示时,折半查找算法无法有效地进行比较操作(排序和查找操作的实现都异常繁琐)。