浅析二分搜索

前言

第一次正儿八经的写了一篇blog,我接触编程也才半年多,才疏学浅,有错误纰漏欢迎指出!

简介

「二分查找 binary search」是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索 范围,直至找到目标元素或搜索区间为空为止。

如果要查找的数据已经事先排好序了,就可以使用二分查找法来进行查找。二分查找法是将

数据分割成两等份,再比较目标值与中间值的大小。如果目标值小于中间值,就可确定要查找的数据在前半部,否则在后半部……如此分割数次直到找到或确定不存在为止。

过程简析

一个简单的二分查找过程如下所示:

图中色块可看作一个有序数组,其中红色色块为需要查找的元素。

首先中间值与键值比较,如下图

此时中间值小于键值,所以对中间值以后的部分查找(此例中中间位置不再包含于下一次查找的范围中,具体后边说),取后半部分的中间位置(下标向前取整,符合整型除法)如下图所示

可以看到此时中间位置仍然小于目标值,对剩余部分的后半部分查找,取其中间位置,如下图

这下中间位置对应的元素等于键值,则找到了键值,查找结束。

代码实现:

力扣(Leetcode)上704题就是一道原汁原味的二分查找,我们以他为例来引入二分查找的代码实现。

题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9

输出: 4

解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2

输出: -1

解释: 2 不存在 nums 中因此返回 -1

AC代码1:

class Solution {

public:

    int search(vector<int>& nums, int target) {

        int n=nums.size(),l=0,r=n-1;

        while(l<=r){

            int m=(l+r)/2;//考虑int类型可能溢出,一般将中间位置下标m的计算公式写为i+(j-i)/2

            if(nums[m]<target){

                l=m+1;

            }

            else if(nums[m]>target)

            r=m-1;

            else

            return m;

        }

        return -1;

    }

};

AC代码2

class Solution {

public:

    int search(vector<int>& nums, int target) {

        int n=nums.size(),l=0,r=n-1;

        while(l<=r){

            int m=(l+r)/2;//考虑int类型可能溢出,一般将中间位置下标m的计算公式写为i+(j-i)/2

            if(nums[m]<target){

                l=m+1;

            }

            else if(nums[m]>target)

            r=m-1;

            else

            return m;

        }

        return -1;

    }

};

上述两组代码都能通过该题,有没有聪明的读者看出来哪里不同呢

揭晓答案:两者分别表示两种不同的范围:左闭右闭和左闭右开

两个代码中,n=nums.size()都表示数组(函数参数)的长度

对于第一种代码,我们发现在初始化右端位置下标r时,我们令r=n-1,则初始时表示即将被搜索的区间为[0,n-1]。对数组nums来说[n-1]是一个有效的位置,可以被取到,所以这段代码表示的就是左闭右闭的一个区间。循环条件就写为while(l<=r){},当l<=r时,继续循环,当l>r退出循环。对于循环体,每次循环要求出当前搜索区间的中间位置m,公式为m=l+r/2中点坐标公式。考虑lr都是int类型,相加可能溢出,一般将中间位置下标m的计算公式写为i+j-i/2。然后拿m位置对应的元素与键值进行比较,如果nums[m]大于键值,则要更新区间的右端点为r=m-1;如果nums[m]小于键值,则要更新左节点为l=m+1;剩下nums[m]等于键值的情况,在元素不重复的前提条件下,这种情况就是找到了键值在数组中的位置,返回此时m的值(整个函数随之结束了),即是键值对应的下标。如果本次循环没找到键值则继续循环,下次循环中查找的就是端点更新后确定的新的搜索区域。如此循环直到找到键值所在位置,或者循环条件不成立,即区间[l,r]为空时(说明键值不存在这个区间中,结束循环后返回-1表示没找到键值),结束循环。

对于第二种代码,我们发现在初始化右端位置下标r时,我们令r=n。对数组nums来说[n]不是一个有效的位置,所以这段代码表示的就是左闭右开的一个区间。循环条件就写为while(l<r){},当l<r时,继续循环,当l=r就退出循环。对于循环体,与上面的情况大致相同,但是更新左右端点的方式有区别。如果nums[m]大于键值,则要更新区间的右端点为r=m;如果nums[m]小于键值,则要更新左节点为l=m+1。其余的和上一种情况相同。

批注1许多人会在是否在循环条件里取等号犯迷糊,我们可以依照下面方法理解:

循环条件中是否取等号与查找的区间有关。

如果是左闭右闭,当l==r成立时——我们假设此时都等于1,下次搜索的区间即为闭区间[11],显然这个区间非空,则说明这个区间是有意义的,所以必须要写相等,否则就少搜索了。

同样的,如果是左闭右开,当l==r时——我们仍然假设l=r=1,下次搜索的区间为[1,1),这个区间是无意义的,所以不能写l==r,写了就多搜索了。

批注2还有许多人在区间的左右端点如何变动上困惑,纠结着什么时候该+1,什么时候不加。这个问题与区间端点是否能取到有关(其实仍然与区间类型有关)。

如果端点包含在区间中,那么变动时就要±1,如果端点不包含于区间中就不用加减。

例如,左闭右闭区间中,两端点都包含于区间中,那么当nums[m]<target时,l=m+1,因为我们已经明确知道nums[m]<target,因为数组有序(递增),所以nums[0]nums[m]都小于target,但是nums[m+1]target的大小关系未知,一直到nums[r]target大小关系都未知。所以下次搜索的区间就应该是nums[m+1]nums[r]因此l=m+1。当nums[m]>target时同理,要变r,左闭右闭区间中已知nums[m]nums[r]均大于键值,nums[l]nums[m-1]target关系大小未知,所以下次搜索区间为nums[l]nums[m-1],即r=m-1

左闭右开区间中,左端点包含于区间中,右端点不包含于区间中。当nums[m]<target时与左闭右闭区间相同;当nums[m]>target时,要变r,已知nums[m]nums[r-1]r取不到)均大于键值,nums[l]nums[m-1]target关系大小未知,所以下次搜索区间为nums[l]nums[m-1],但是因为nums[r]不包含于搜索区间中,所以要将m-1+1才能得到下次搜索时r的取值,即r=m(为什么不-1呢?有没有可能-1之后每次变右端点之后都要少搜一个元素?)。

二分搜索的优点与局限性:

优点:

  1. 时间复杂度为O(log n),相比直接遍历查找的方法效率较高
  2. 二分查找无须额外空间。相较于需要借助额外空间的搜索算法(例如哈希查找),二分查找更加节省空间。

局限性:

  1. 二分查找仅适用于有序数据。若输入数据无序,为了使用二分查找而专门进行排序,得不偿失。因为排序算法的时间复杂度通常为O(n log n)比线性查找和二分查找都更高。对于频繁插入元素的场景,为保持数组有序性,需要将元素插入到特定位置,时间复杂度为O(n)也是非常昂贵的。
  2. 二分查找仅适用于数组。二分查找需要跳跃式(非连续地)访问元素,而在链表中执行跳跃式访问的效率较低,因此不适合应用在链表或基于链表实现的数据结构。
  3. 小数据量下,线性查找性能更佳。在线性查找中,每轮只需 1 次判断操作;而在二分查找中,需要 1 次加法、1 次除法、1 ~ 3 次判断操作、1 次加法(减法),共 4 ~ 6 个单元操作;因此,当数据量n较小时,线性查找反而比二分查找更快。

二分搜索的使用

EG1

Leetcode 35.搜索插入位置

给定一个排序的整数数组 nums 和一个整数目标值 target ,请在数组中找到 target ,并返回其下标。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

输入: nums = [1,3,5,6], target = 5

输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2

输出: 1

示例 3:

输入: nums = [1,3,5,6], target = 7

输出: 4

示例 4:

输入: nums = [1,3,5,6], target = 0

输出: 0

示例 5:

输入: nums = [1], target = 0

输出: 0

 

提示:

1 <= nums.length <= 104

-104 <= nums[i] <= 104

nums 为无重复元素的升序排列数组

-104 <= target <= 104

AC代码:

class Solution {

public:

    int searchInsert(vector<int>& nums, int target) {

    int n=nums.size(),l = 0, r = n - 1;

    while (l <= r) {

        int m = (l + r) / 2;

        if (nums[m] < target)

            l = m + 1;

        else if (nums[m] > target) {

            r = m - 1;

        }

        else return m;

    }

    return l;

    }

};

(当区间为左闭右开同样能过,自己试试吧懒得粘了)

这一题思路没啥说的,典型的二分搜索。当target不存在于数组中时,对于左闭右闭区间,循环结束的条件是l>r,此时数组中必有两个元素数值的大小将target夹在中间,且l指向较大的元素,r指向较小的元素,我们要在较大的元素前边插入target,要将此时l指向的这个数据和其后边的数据统一后移一位,然后再将target插入l处。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值