今天,我们学习的是排序查找算法中,最容易理解并且掌握的二分查找。
二分查找又名折半查找。折半查找的前提要求,即所查询的数组必须是一个有序数组。(无论升序降序)只有当所查询数据是规律排布的情况下,我们才可以进行查找。(后面会详细解释为什么必须是有序数组)
二分查找的优点
倘若给定上述数组,你打算如何寻找元素5呢?
我们先来看看遍历数组的查找过程。首先从数组首元素开始判断,1×、2×、2×、3×、4×、5√。我们可以看到比较次数为6次。
而采用二分查找需要几次呢?标定mid = (left+right)/2,mid=3<5。此时left = mid+1,mid=5。mid = 5,我们找到了元素5,整个过程只需要两次。
通过上述两个例子的对比,我们不难看出,遍历数组的时间复杂度O(n)相较于二分查找的时间复杂度O(logn),同样查找情况下,二分查找的速度远快于遍历。
二分查找的过程
首先我们来看二分查找的查找方式。 在查找前,我们需要做三个准备工作。
1、确定查找数组是否有序,以及数组的长度。
2、标定起始位置和终止位置。
3、在起始位置与中止位置之间,标定中间位置。(这一步是二分查找的关键)
接下来我们推演二分查找的过程(假定我们查找元素5):

从给定数组[1,2,2,3,4,5,6]可以得知,该有序数组长度为7,起始位置标定为0,终止位置标定为6,则中间位置mid标定为(0+6)/2=3。此时,我们已经做好了二分查找的前期准备工作。
int arr[] = { 1,2,2,3,4,5,6 };
int num = 0;
printf("请输入要查询的元素:");
scanf("%d", &num);
int arrSize = sizeof(arr) / sizeof(int);
int left = 0;
int right = arrSize-1;
int mid = (left + right)/2;
接下来进入查找环节。
我们已知mid下标里的元素是3。3<5,所以应当在mid-right的区间里去继续查找(这时候必须要求数组的有序性,因为无序数组无法判断左右两边的大小,所以数组有序性是二分查找的关键)

此时,我们抛弃了整个数组大区间,因为我们知道mid是小于目标数字的,所以比mid小的区间内数字应该全部剔除,包括mid。我们就得到了第二次查找到起始状态
int mid = (right + left) / 2;
if (arr[mid] < num) //如果mid<所查询的数字,我们应当抛弃左边小于mid的空间
{
left = mid + 1;
}
else if (arr[mid] > num) //如果mid>所查询的数字,我们应当抛弃右边大于mid的空间
{
right = mid - 1;
}
新的left = mid + 1,right保持不变,新的mid继续保持mid = (left + right)/2 = 5。此时我们再对比mid和输入的数字可得,mid == 5。查找结束,我们已经找到了需要的数字。
倘若我们希望查找7这个元素(可知当前数组内没有7这个元素)。这时,我们应当如何终止查找呢?
我们可知,当循环到最后一次时,7是大于整个数组内所有元素的,这个时候left已经和right重合了。查询区间内,已经没有可以遍历的元素。于是结束循环后,left=7,而right依然等于6。此时我们只需要判断left是否大于right,就可以知道整个数组内没有符合条件的元素了,因为如果符合条件,在之前的循环就会跳出,从而使得left最多与right相等(数组的最后一个元素),不会出现left大于right的情况。
while (left <= right)
{
int mid = (right + left) / 2;
if (arr[mid] < num) //如果mid<所查询的数字,我们应当抛弃左边小于mid的空间
{
left = mid + 1;
}
else if (arr[mid] > num) //如果mid>所查询的数字,我们应当抛弃右边大于mid的空间
{
right = mid - 1;
}
else
{
printf("找到了,元素下标为:%d\n", mid);
break;
}
}
if(left > right) //此时已经遍历完整个数组,依然没有寻找到给定数字
{
printf("没找到此元素\n");
}
小结
我们可以知道,二分查找是一种非常优秀、并且基础的查找算法。它的原理通俗易懂,即不断地抛弃掉不符合条件的区间,缩小区间,简化查找步骤。但是它也是一种局限性非常强的查找算法——必须在有序空间内进行查找!倘若数据是无序存放的,又何谈mid左右区间的大小呢?