二分查找:我怀疑你持有违禁品,请接受检查 0^0!

101 阅读7分钟

前言

经过了两个排序的沉淀和历练,不知道大家是不是已经对算法有了一些初步了解了呢?在我们之前学习的快速排序和归并排序中,有一个很重要的思想——————分区,它能将数组分成两半来分别处理,这一种思想,使得我们在处理数据中更加方便,而它的好处,将会在我们今天学习的二分查找中体现的淋漓尽致。

0C12804B6E897EC9A5AAA13A0DCE7F1E.jpg

附上前两期地址:那个更优秀的男人:快速排序(小白专用0v0) - 掘金

归并排序:递归的原理,你弄懂了吗?(Merge Sort) - 掘金

举个栗子

举个栗子.jpg 话不多说,我们来举个栗子看看二分查找是如何实现的。 首先有这么一个数组:

图片1.png 我们需要下标int l,int r,int mid(取中点),我们要标记出需要查找到的数字。

图片3.png 如果我们的target>a[mid],则就使得l=mid+1,

图片4.png 如果a[mid]==target,那么就将target的下标返回,表示已经找到了目标数字所在数组的位置。

反过来,如果target<a[mid],那么就使得r=mid-1;

图片5.png

图片6.png 一直将target和a[mid]比较,通过不断变换l和r的位置来找到target所在位置。

图片7.png

代码实现

其具体实现代码如下:

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能行吗

为什么在例子里的数组是有序的,如果换成无序的那么二分查找还能用吗?

首先我们来解决一下这个问题,二分查找的核心思想是基于有序数组或序列,通过将搜索区间一分为二来逐步缩小目标值的查找范围,从而有效地减少查找的时间复杂度。它的基本思想是通过不断地排除一半不可能包含目标值的元素,最终将搜索范围缩小到目标元素所在的子区间。

如果数组里的数据是无序的,那么我们的二分查找其实就没有什么意义了,我们运用二分查找,就是要找数据找的快,它只对于有序的数据起作用。大家能看到我们是运用二分的办法,每一次都将搜索范围缩小一半,而能这么做的原因就是因为我们的数组是有序的,再拿上面的例子来说:

图片3.png 我们已经知道了a[mid]=7<target在这种情况下从a[l]到a[mid]的数据全都比target要小,所以我们才能把它们全部忽略掉,直接使得l=mid+1,从a[4]开始继续二分。

如果我们组的数据像{3,2,5,8,3,5,7,0}这样无序,二分查找就不具有正确性了,这个时候还不如直接运用for循环一个个遍历来找到答案。

面对无序数组时二分法的运用

当我们面对无序数组时难道就没有办法来解决了吗?2C8A977359DC22E8961AB06F4344BB3A.jpg

也不是,我们也可以通过之前学的归并和快速排序,将无序的数组变得有序起来,再来使用二分查找,鉴定目标数据是不是在数组中。

为什么比较了a[mid]和target后要使得l=mid+1,或者r=mid-1?为什么在while循环中的条件是l<=r呀?l<r能行吗?

对于这两个问题,我们要放到一起来解决,由因结果,种什么因就结什么果~循环的条件是l和r取值的因,l和r的取值是循环条件的果

0B58D23FE80F9FC16EADAFDF0DC25218.jpg

在二分查找中,循环的条件决定了开闭区间,如果条件为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,最后一个元素符合条件,返回下标,二分查找结束。

图片5.png 图片6.png

图片7.png 接下来我们来利用左闭右开

图片5.png 因 target<a[mid],所以r=mid;

图片8.png

图片9.png 此时我们可以看到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; 
}

那么我们再来看看上面的例子:

图片5.png 在这一段代码逻辑中,由于a[mid]>target,所以r=mid-1;

图片6.png 再经过判断:a[mid]<target,l=mid+1;

图片10.png 此时由于l=r,不符合进入while循环的条件,mid仍然为1,返回值为-1,表示没找到想要的结果。 由于搭配不当,引起二分查找漏元素,导致结果不对。

C131A90DC089B4613E9FD7FDF46A436C.gif

总结

针对于二分查找,我们之前已经学过了不少二分的思想,在这里学习起来应该没有太大的难度,但是要注意,二分仅仅适用于有序的数据(升序或者降序),其时间复杂度为O(logn)。在无序的数组中查找目标值,建议使用排序将数据排好序,再使用二分查找。其次要注意二分排序的区间开闭,通过while循环的条件,判断l和r更新的值是l=mid+1,r=mid-1,还是l=mid+1和r=mid。不要搭配错误导致漏选元素或者进入死循环。 好了,这一期就是这样了,如果有什么不懂的,或者我的文章有什么错误,欢迎大家指出!拜拜! 8.gif

本期学习资料来源

Chat GPT

手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili

Acwing

二分法知道原理,但是总也写不对?手把手教你彻底搞定它!_哔哩哔哩_bilibili

Leetcode 33

Leetcode 34

Leetcode 35

Leetcode 74

Leetcode 81

Leetcode 153

Leetcode 154

温馨提示:二分查找在部分有序(有序+无序)的数组中通过一定方法也可以使用,但是我还没学明白233333333.