前言
经过了两个排序的沉淀和历练,不知道大家是不是已经对算法有了一些初步了解了呢?在我们之前学习的快速排序和归并排序中,有一个很重要的思想——————分区,它能将数组分成两半来分别处理,这一种思想,使得我们在处理数据中更加方便,而它的好处,将会在我们今天学习的二分查找中体现的淋漓尽致。
附上前两期地址:那个更优秀的男人:快速排序(小白专用0v0) - 掘金
归并排序:递归的原理,你弄懂了吗?(Merge Sort) - 掘金
举个栗子
我们需要下标int l,int r,int mid(取中点),我们要标记出需要查找到的数字。
如果我们的target>a[mid],则就使得l=mid+1,
如果a[mid]==target,那么就将target的下标返回,表示已经找到了目标数字所在数组的位置。
反过来,如果target<a[mid],那么就使得r=mid-1;
一直将target和a[mid]比较,通过不断变换l和r的位置来找到target所在位置。
代码实现
其具体实现代码如下:
int binarysearch(int a[];int n;int target)
{ int l=0;
int r=n-1;
while(l<=r){
int mid=(l+r)/2;
if(a[mid]==target){
return mid;
}
else if(a[mid]<target){
l=mid+1;
}
else r=mid-1;
}
return -1;
相关疑问的解答
相信大家在看完了上面的例子和代码后总会有那么些疑问,比如:
为什么在你例子里的数组是有序的,如果换成无序的那么二分查找还能用吗?
为什么比较了a[mid]和target后要使得l=mid+1,或者r=mid-1啊?
为什么在while循环中的条件是l<=r呀?l<r能行吗
为什么在例子里的数组是有序的,如果换成无序的那么二分查找还能用吗?
首先我们来解决一下这个问题,二分查找的核心思想是基于有序数组或序列,通过将搜索区间一分为二来逐步缩小目标值的查找范围,从而有效地减少查找的时间复杂度。它的基本思想是通过不断地排除一半不可能包含目标值的元素,最终将搜索范围缩小到目标元素所在的子区间。
如果数组里的数据是无序的,那么我们的二分查找其实就没有什么意义了,我们运用二分查找,就是要找数据找的快,它只对于有序的数据起作用。大家能看到我们是运用二分的办法,每一次都将搜索范围缩小一半,而能这么做的原因就是因为我们的数组是有序的,再拿上面的例子来说:
我们已经知道了a[mid]=7<target,在这种情况下从a[l]到a[mid]的数据全都比target要小,所以我们才能把它们全部忽略掉,直接使得l=mid+1,从a[4]开始继续二分。
如果我们组的数据像{3,2,5,8,3,5,7,0}这样无序,二分查找就不具有正确性了,这个时候还不如直接运用for循环一个个遍历来找到答案。
面对无序数组时二分法的运用
当我们面对无序数组时难道就没有办法来解决了吗?
也不是,我们也可以通过之前学的归并和快速排序,将无序的数组变得有序起来,再来使用二分查找,鉴定目标数据是不是在数组中。
为什么比较了a[mid]和target后要使得l=mid+1,或者r=mid-1?为什么在while循环中的条件是l<=r呀?l<r能行吗?
对于这两个问题,我们要放到一起来解决,由因结果,种什么因就结什么果~循环的条件是l和r取值的因,l和r的取值是循环条件的果。
在二分查找中,循环的条件决定了开闭区间,如果条件为l<=r,那么则为左闭右闭,如果为l<r,则为左闭右开,而区间的开闭只会决定最后一个元素的查找。
int binarySearch(int arr[], int n, int target) {
int l = 0, r = n-1; //左闭右闭
while (l <= r) {
int mid = l + (r - l) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
l = mid + 1;
else r = mid-1;
}
return -1;
}
int binarySearch(int arr[], int n, int target) {
int l = 0, r = n-1; //左闭右开
while (l < r) {
int mid = l + (r - l) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
l = mid + 1;
else r = mid;
}
return -1;
}
让我们来看同一个例子:l=0,r=2,mid=1,target=5=a[2]; 利用左闭右闭的方法来写,l=r时,进入了循环,此时l=r=mid,最后一个元素符合条件,返回下标,二分查找结束。
接下来我们来利用左闭右开:
因 target<a[mid],所以r=mid;
此时我们可以看到l和r并没有重合,这就是两个区间不同的区别,即左闭右闭区间会检查最后一个元素,而左闭右开则不会。
关于区间的注意:l<=r时,更新区间只能用l=mid+1,r=mid-1,
l<r时,l=mid+1,r=mid,因为在最后r下标指向的数据是取不到的。
搭配不当会怎么样?
假设你写了一个左闭右开的二分,而区间更新的l和r取值不对。
int binarySearch(int arr[], int n, int target) {
int l = 0, r = n-1; //左闭右开
while (l < r) {
int mid = l + (r - l) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
l = mid + 1;
else r = mid-1; //错误,应为r=mid;
}
return -1;
}
那么我们再来看看上面的例子:
在这一段代码逻辑中,由于a[mid]>target,所以r=mid-1;
再经过判断:a[mid]<target,l=mid+1;
此时由于l=r,不符合进入while循环的条件,mid仍然为1,返回值为-1,表示没找到想要的结果。
由于搭配不当,引起二分查找漏元素,导致结果不对。
总结
针对于二分查找,我们之前已经学过了不少二分的思想,在这里学习起来应该没有太大的难度,但是要注意,二分仅仅适用于有序的数据(升序或者降序),其时间复杂度为O(logn)。在无序的数组中查找目标值,建议使用排序将数据排好序,再使用二分查找。其次要注意二分排序的区间开闭,通过while循环的条件,判断l和r更新的值是l=mid+1,r=mid-1,还是l=mid+1和r=mid。不要搭配错误导致漏选元素或者进入死循环。
好了,这一期就是这样了,如果有什么不懂的,或者我的文章有什么错误,欢迎大家指出!拜拜!
本期学习资料来源
Chat GPT
手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili
Acwing
二分法知道原理,但是总也写不对?手把手教你彻底搞定它!_哔哩哔哩_bilibili
Leetcode 33
Leetcode 34
Leetcode 35
Leetcode 74
Leetcode 81
Leetcode 153
Leetcode 154
温馨提示:二分查找在部分有序(有序+无序)的数组中通过一定方法也可以使用,但是我还没学明白233333333.