Bootstrap

【优选算法】二分查找

在这里插入图片描述

二分查找总结

首先这里对二分查找做一些了解

一、算法原理

二分查找不一定非要在数组有序的情况下才能使用,在某些有规律的情况下也同样适用。

使用二分查找的前提是这段数据具有 二段性”,二段性就是在这段数据中随便找一个点就能将这段数据分为两段,并且可以按照规律舍弃一段。


二、模版

对于下面的三种模版,希望大家不要死记硬背,最好在理解的基础上再记忆

  1. 朴素的二分查找模版

    首先定义三个变量 left(指向数据首部)、right(指向数据的尾部)和 mid(指向数据的中间)。(target是目标值,假设这么数组名叫arr

    朴素的二分查找模版的核心就是:

    arr[mid] > target --> right = mid - 1 , mid = left + (right - left) / 2

    arr[mid] < target --> left = mid + 1 , mid = left + (right - left) / 2

    arr[mid] == target --> 返回 mid

    朴素的二分查找模版中的细节问题:
    循环条件left <= right

    二分查找的正确性:二分查找通过利用待查找序列的有序性,并采用逐步逼近和缩小查找范围的策略,保证了其正确性。正确处理边界情况也是保证算法正确性的重要因素之一

    时间复杂度O( l o g 2 N log_2N log2N)

    朴素的二分查找的代码模版:

    // 这里的...需要根据具体的题目来填写
    while (left <= right)
    {
        // 防溢出
        int mid = left + (right - left) / 2;
        if (...)
            right = mid - 1;
        else if (...)
            left = mid + 1;
        else
            return ...;
    }
    

  1. 查找左边界的二分查找模版
    使用二分查找的前提就是数据具有两段性,这里左边界(最靠近 lefttarget 相等的值就是左边界)是我们需要查找的结果,那么根据二段性可以将数据分为两段 [ left , 左边界左边的位置] [左边界位置 , right ]

    首先定义三个变量 left(指向数据首部)、right(指向数据的尾部)和 mid(指向数据的中间)。(target是目标值,假设这么数组名叫arr

    查找左边界的二分查找模版的核心就是:

    arr[mid] >= target --> right = mid , mid = left + (right - left) / 2
    由于这里的=并不一定是我们想要的结果,所以这里与>的情况合并,因为这里 mid 可能是左边界,所以这里 right 不能变为mid-1,否则改变后的[ left , right ]这段区间内会找不到左边界。

    arr[mid] < target --> left = mid + 1 , mid = left + (right - left) / 2
    因为这里 [ left , mid ]这个区间内的数据全部小于target,所以这段区间中不可能存在左边界,所以这里 left = mid + 1

    查找左边界的二分查找可能出现的三种情况:
    [left , right] 这个范围中有值等于 target
    leftright 最终重叠在左边界处

    [left , right] 这个范围中值都小于 target
    left 在移动过程中不会改变,leftright 最终重叠在 left

    [left , right] 这个范围中值都大于 target
    right 在移动过程中不会改变,leftright 最终重叠在 right

    leftright 重叠后,判断 leftright 指向的元素是否与 target 是否相等,来判断 leftright 指向的位置是否为左边界。

    查找左边界的二分查找模版中的细节问题:
    循环条件left < right
    ------⑴ 当 left == right 就知道结果了,不需要继续循环了
    ------⑵ 继续循环会导致死循环,因为符合arr[mid] >= target ,那么下一步就是right = mid,而这里 mid 改变后与原来的值相同,那么 right 的值也不会改变,一直满足left == right这个条件,所以会造成死循环。

    求中点的操作
    ------⑴ mid = left + ( right - left ) / 2 ------- √
    当数据个数为偶数个时,这个操作会选择最中间两个元素中左边的一个,arr[mid] target 满足什么关系,都可以使 leftright 重叠,从而结束循环。
    ------⑵ mid = left + ( right - left - 1) / 2 ------- ×
    当数据个数为偶数个时,这个操作会选择最中间两个元素中右边的一个,若这里只有两个元素,left 指向左边的一个元素,right指向右边的元素,如果满足arr[mid] < target这个条件,leftright 可以重叠从而结束循环。如果满足arr[mid] >= target这个条件,那么 mid 改变后是与right指向同一个位置,而right = mid的操作使 leftright 不能重叠,从而变为死循环。

    查找左边界的二分查找的代码模版:

    // 这里的...需要根据具体的题目来填写
    while (left < right)
    {
        // 防溢出
        int mid = left + (right - left) / 2;
        if (...)
            left = mid + 1;
        else
            right = mid;
    }
    

  1. 查找右边边界的二分查找模版

    使用二分查找的前提就是数据具有两段性,这里右边界(最靠近 righttarget 相等的值就是右边界)是我们需要查找的结果,那么根据二段性可以将数据分为两段 [ left , 右边界的位置] [右边界右边的位置 , right ]

    首先定义三个变量 left(指向数据首部)、right(指向数据的尾部)和 mid(指向数据的中间)。(target是目标值,假设这么数组名叫arr

    arr[mid] > target --> right = mid - 1, mid = left + (right - left +1) / 2
    因为这里 [ mid , right]这个区间内的数据全部大于target,所以这段区间中不可能存在右边界,所以这里 right = mid - 1

    arr[mid] <= target --> left = mid , mid = left + (right - left + 1) / 2
    因为这里 mid 可能是右边界,所以这里 right 不能变为mid-1,否则改变后的[ left , right ]这段区间内会找不到右边界。

    查找右边界的二分查找可能出现的三种情况:
    [left , right] 这个范围中有值等于 target
    leftright 最终重叠在右边界处

    [left , right] 这个范围中值都小于 target
    right 在移动过程中不会改变,leftright 最终重叠在 right

    [left , right] 这个范围中值都大于 target
    left 在移动过程中不会改变,leftright 最终重叠在 left

    leftright 重叠后,判断 leftright 指向的元素是否与 target 是否相等,来判断 leftright 指向的位置是否为右边界。

    查找右边界的二分查找模版中的细节问题:
    循环条件left < right
    ------⑴ 当 left == right 就知道结果了,不需要继续循环了
    ------⑵ 继续循环会导致死循环,因为符合arr[mid] <= target ,那么下一步就是left= mid,而这里 mid 改变后与原来的值相同,那么 left 的值也不会改变,一直满足left == right这个条件,所以会造成死循环。

    求中点的操作
    ------⑴ mid = left + ( right - left ) / 2 ------- ×
    当数据个数为偶数个时,这个操作会选择最中间两个元素中左边的一个,若这里只有两个元素,left 指向左边的一个元素,right指向右边的元素,如果满足arr[mid] > target这个条件,leftright 可以重叠从而结束循环。如果满足arr[mid] <= target这个条件,那么 mid 改变后是与left指向同一个位置,而left = mid的操作使 leftright 不能重合重叠,从而变为死循环。
    ------⑵ mid = left + ( right - left - 1) / 2 ------- √
    当数据个数为偶数个时,这个操作会选择最中间两个元素中右边的一个,若这里只有两个元素,left 指向左边的一个元素,right指向右边的元素,那么 mid 改变后是与right指向同一个位置,而left = mid的操作可以使 leftright 重叠。

    查找右边界的二分查找的代码模版:

    // 这里的...需要根据具体的题目来填写
    while (left < right)
    {
        // 防溢出
        int mid = left + (right - left + 1) / 2;
        if (...)
            left = mid;
        else
            right = mid - 1;
    }
    

一、二分查找

题目描述
在这里插入图片描述

思路讲解
本题有一个暴力解法就是遍历数组,每遍历一个数字就可以淘汰一个数字,直到找到目标数字或遍历完数组。

由于这个数组是升序的,我们在数组中随便选中一个数字,用这个数字与目标数字进行对比,相同则返回下标,不同则可以以当前数字为分为两边,淘汰一边的数字,选的数字可以是数组中的二等分点,三等分点等任意位置,但是选用二等分点的效率最高。

由于本题只需要找到目标数字,所以使用朴素版本的二分查找就可以完成本题,首先定义三个变量left、mid和right,left为数组的最左边,right为数组的最右边,mid为数组的中间位置。将mid位置上的数字与目标数字的进行对比,相同返回mid,比目标数字小则淘汰包括mid位置左边的所有数字,将left置为mid+1,比目标数字大则淘汰包括mid位置右边的所有数字,将right置为mid-1,一直循环直到找到目标数字或是left>right,由于当left==right时,当前下标的数字可能就是目标数字,所以循环条件是left<=right。

编写代码

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0 , right = nums.size() - 1;
        while(left <= right)
        {
            // int mid = (left + right)/2; // 数据量太大可能会溢出
            int mid = left + (right - left) / 2;
            if(nums[mid] > target)
                right = mid - 1;
            else if(nums[mid] < target)
                left = mid + 1;
            else
                return mid;
        }
        return -1;
    }
};

二、在排序数组中查找元素的第一个和最后一个位置

题目描述
在这里插入图片描述

思路讲解
本题最简单的思路就是使用暴力解法,遍历整个数组找到目标数字的范围。

由于本题需要目标数字的下标范围,所本题使用二分查找时,不能使用朴素版本的二分查找,需要使用查找左边界和右边界的二分查找来查找目标数字的范围。具体查找左右边界的方法在上面总结中讲到了,大家可以仔细查看方法。本题还需要处理数组中没有元素的特殊情况,否则使用二分查找时会导致越界的情况。

编写代码

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int numsSize = nums.size();
        int left = 0 , right = numsSize-1;
        int ansLeft = 0 , ansRight = 0;

        if(numsSize == 0)
            return {-1,-1};

        // 查找左边界
        while(left < right)
        {
            int mid = left + (right - left) / 2;

            if(nums[mid] < target)
                left = mid + 1;
            else // (nums[mid] > target)
                right = mid;
        }

        if(nums[left] == target)
            ansLeft = left;
        else
            return {-1,-1};
        
        left = 0 , right = numsSize - 1;
        // 找右边界
        while(left < right)
        {
            int mid = left + (right - left + 1) / 2;

            if(nums[mid] <= target)
                left = mid;
            else // (nums[mid] > target)
                right = mid - 1;
        }

        if(nums[left] == target)
        {
            ansRight = right;
            return {ansLeft,ansRight};
        }
        else
            return {-1,-1};
    }
};

三、搜索插入位置

题目描述
在这里插入图片描述

思路讲解
本题需要在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置,按顺序插入的位置就是第一个比目标值大的数字的下标。

使用二分查找之前,我们需要找到数组的二段性,以下图为例,我们可以发现,我们找到的位置正好与target相等处的位置,或是数组中没有target,在数组中查找插入位置,要么第一个大于target的位置插入,要么target大于数组中所有的数,插入位置为最后一个位置的下一个位置。根据上面的信息我们就能以目标值为中点将整个数组分为两段,一段是小于目标值的数组区间,另一段就是大于等于目标值的数组区间。

在这里插入图片描述

分为上面两个区间后,我们知道需要查找的位置一定会在大于等于target的区间中,所以我们只需要找到大于等于target的区间中左端点即可。

接下来我们对left和right的移动方式进行分类讨论,这里设mid位置上的数字为x:
①当x<target时,我们可以得知mid指向的是小于target的区间,这个区间内不可能出现我们所需要的答案,接下来就需要在右边的区间进行查找,又因为mid指向的数字不可能是我们所需要的答案,那么left可以直接跳过mid,所以left的移动方式为:left=mid+1
②当x>=target时,我们可以得知mid指向的是大于等于target的区间,这个区间内可能会出现我们所需要的答案,接下来就需要在左边的区间进行查找,但是mid指向的数字可能是我们所需要的答案,那么right不能直接跳过mid,所以right的移动方式为:right=mid

对上面left和right的移动方式进行讨论后,我们就可以套用模版进行代码的编写了,mid如何计算,我们只需要记得,在left和right的移动操作中有-1,那么mid计算时就要多一个+1,具体可以看上面的模版总结。但是上面的操作只能解决正好找到target或是插入位置在数组中的情况,若是target比数组中所有的数都要大时,left和right最终指向的位置是数组的最后一个位置,所以left和right指向数组中最后一个位置的时候,需要进行判断,相同则返回最后一个位置,不相同则返回最后一个位置的下一个位置。

编写代码

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0 , right = nums.size() - 1;

        if(target < nums[0])
            return 0;

        while(left < right)
        {
            int mid = left + (right - left + 1) / 2;
            if(nums[mid] <= target)
                left = mid;
            else
                right = mid - 1;
        }

        if(nums[left] == target)
            return left;
        else
            return left + 1;
    }
};

四、x 的平方根

题目描述
在这里插入图片描述

思路讲解
本题给你一个非负整数 x,计算并返回 x 的算术平方根。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。

根据上面的描述,我们可以使用暴力解法,从0开始向后平方,若平方得到的结果小于题目所给的数,则继续向后进行平方操作,若平方得到的结果等于题目给的数则返回平方前的数,若平方得到的结果大于题目给的数,则返回平方前数的前一个数。

当我们对暴力解法的操作进行观察时,发现我们得到答案的平方一定是小于或等于x的,那么我们就发现了本题的二段性,则本题可以使用二分查找来解决问题,根据条件我们可以将数组分为两段,一段是包括以答案左边所有的数平方后小于等于给出的数字,另一段是答案右边的数平方后大于给出的数字。

接下来我们对left和right的移动方式进行分类讨论,这里设mid位置上的数字为ret:
①当ret2<=x时,我们可以得知mid指向数字的平方是小于等于x的,也就是小于等于x的区间,这个区间内可能出现我们所需要的答案,接下来就需要在右边的区间进行查找,又因为mid指向的数字可能是我们所需要的答案,那么left不能直接跳过mid,所以left的移动方式为:left=mid

②当ret2>x时,我们可以得知mid指向的数字的平方是大于x的,也就是大于x的区间,这个区间内不可能会出现我们所需要的答案,接下来就需要在左边的区间进行查找,但是mid指向的数字不可能是我们所需要的答案,那么left可以直接跳过mid,所以right的移动方式为:right=mid-1

对上面left和right的移动方式进行讨论后,我们就可以套用模版进行代码的编写了,mid如何计算,我们只需要记得,在left和right的移动操作中有-1,那么mid计算时就要多一个+1,具体可以看上面的模版总结。
在这里插入图片描述

编写代码

class Solution {
public:
    int mySqrt(int x) {
        long long left = 0 , right = x;
        while(left < right)
        {
            long long mid = left + (right - left + 1) / 2;
            if(mid * mid <= x)
                left = mid;
            else
                right = mid - 1;
        }
        return left;
    }
};

五、山脉数组的峰顶索引

题目描述
在这里插入图片描述

思路讲解
对本题的模型进行抽象,就出现了下面这种情况,数字的大小先上升后下降,峰值出现的位置比左右两边的数都要大,所以使用暴力解法很容易就能解决本题,这里不对暴力解法进行讲解。

通过下图我们很容易就看出来本题的二段性,所以本题可以使用二分查找来解决,根据下图我们可以将数组分为两段,一段是小于等于峰值的左区间,另一段是大于峰值大于峰值的右区间,需要注意一个细节就是最左边和最右边的数不可能是峰值。

在这里插入图片描述

接下来我们对left和right的移动方式进行分类讨论:
①当arr[mid-1]<=arr[mid]时,我们可以得知mid-1指向数字是小于等于mid指向数字的,这个区间内可能出现我们所需要的答案,接下来就需要在右边的区间进行查找,又因为mid指向的数字可能是我们所需要的答案,那么left不能直接跳过mid,所以left的移动方式为:left=mid

②当arr[mid-1]>arr[mid]时,我我们可以得知mid指向数字是大于mid+1指向数字的,这个区间内不可能会出现我们所需要的答案,接下来就需要在左边的区间进行查找,但是mid指向的数字不可能是我们所需要的答案,那么left可以直接跳过mid,所以right的移动方式为:right=mid-1

对上面left和right的移动方式进行讨论后,我们就可以套用模版进行代码的编写了,mid如何计算,我们只需要记得,在left和right的移动操作中有-1,那么mid计算时就要多一个+1,具体可以看上面的模版总结。

编写代码

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 1, right = arr.size() - 2;
        while (left < right)
        {
            int mid = left + (right - left + 1) / 2;
            if (arr[mid-1] < arr[mid])
                left = mid;
            else 
                right = mid - 1;
        }
        return left;
    }
};

六、寻找峰值

题目描述
在这里插入图片描述

思路讲解
通过对题目的解析可以发现,峰值有三种出现的情况,首先是先上升再下降,最高点就是顶峰,然后是一开始就一直上升,最后一个位置就是顶峰,最后是一最开始的位置就是顶峰。

在这里插入图片描述

本题的暴力解法就是从头向后变量,进行分类讨论即可,不做过多讲解。

接下来对暴力解法进行优化,在数组中找一个位置i,与i+1位置上的数字进行比较会出现两种情况:

nums[i]>nums[i+1],我们就可以知道包括i+1之前的这段区间中一定有一段区间是呈下降趋势的,那么包括i之前的这段区间中一定有一个峰值,而i之和的这段区间可能一直呈下降趋势,不一定有峰值的情况。
nums[i]<nums[i+1],我们就可以知道包括i之后的这段区间中一定有一段区间是呈上升趋势的,那么包括i+1之和的这段区间一定有一个峰值,而i+1之前的这段区间可能一直呈上升趋势,不一定有峰值的情况。

根据上面的情况我们可以发现这道题的二段性,所以这道题可以使用二分查找来解决。

接下来我们对left和right的移动方式进行分类讨论:
①当nums[mid]<nums[mid+1]时,我们可以得知mid指向数字是小于等于mid+1指向数字的,那么在[mid+1,right] 这个区间内一定会出现峰值,而在[left,mid]这个区间内不一定有峰值,接下来就需要在[mid+1,right] 这个区间进行查找,又因为mid+1指向的数字可能是我们所需要的答案,所以left的移动方式为:left=mid+1

②当nums[mid]>nums[mid+1]时,我们可以得知mid指向数字是大于mid+1指向数字的,那么在[left,mid] 这个区间内一定会出现峰值,而在[mid+1,right]这个区间内不一定有峰值,接下来就需要在[left,mid] 这个区间进行查找,又因为mid指向的数字可能是我们所需要的答案,所以left的移动方式为:left=mid

对上面left和right的移动方式进行讨论后,我们就可以套用模版进行代码的编写了,mid如何计算,我们只需要记得,在left和right的移动操作中有-1,那么mid计算时就要多一个+1,具体可以看上面的模版总结。

编写代码

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left = 0 , right = nums.size() - 1;
        while(left < right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid] < nums[mid + 1])
                left = mid + 1;
            else
                right = mid;
        }
        return left;
    }
};

七、寻找旋转排序数组中的最小值

题目描述
在这里插入图片描述

思路讲解
对本题的模型进行抽象,就出现了下面两种情况,我们以最左边/最右边的数作为参照物,就可以发现本题的二段性了,这里以最右边的数作为参照物进行讲解,我们可以发现[A,B]区间的数都比最右边的数要大,[C,D]区间上的数都比最右边的数小或相等。
在这里插入图片描述

接下来我们对left和right的移动方式进行分类讨论,定义一个变量flag指向数组的最右边的数:
①当nums[mid]<=nums[flag]时,我们可以得知mid指向数字是小于等于数组最右边的数,那么在[left,mid] 这个区间内一定会出现最小值,而在[mid+1,right]这个区间内不会存在最小值,接下来就需要在[left,mid] 这个区间进行查找,又因为mid指向的数字可能是我们所需要的答案,所以left的移动方式为:right=mid

②当nums[mid]>nums[flag]时,我们可以得知mid指向数字是大于数组最右边的数,那么在[mid+1,right] 这个区间内一定会出现最小值,而在[left,mid]这个区间内不会存在最小值,接下来就需要在[mid+1,right] 这个区间进行查找,又因为mid+1指向的数字可能是我们所需要的答案,所以left的移动方式为:left=mid+1

当数组旋转为原数组时,以最右边的数作为参照物可以使mid一直数字更小的位置移动,最终找到最小值,所以以最右边的数作为参照物就不存在边界情况。


上面讲到的是以最右边的数作为参考值,这里以最左边的数作为参考值进行讲解,我们对left和right的移动方式进行分类讨论,定义一个变量flag指向数组的最左边的数:
①当nums[mid]<nums[flag]时,我们可以得知mid指向数字是小于等于数组最左边的数,那么在[left,mid] 这个区间内一定会出现最小值,而在[mid+1,right]这个区间内不会存在最小值,接下来就需要在[left,mid] 这个区间进行查找,又因为mid指向的数字可能是我们所需要的答案,所以left的移动方式为:right=mid

②当nums[mid]>=nums[flag]时,我们可以得知mid指向数字是大于数组最右边的数,那么在[mid+1,right] 这个区间内一定会出现最小值,而在[left,mid]这个区间内不会存在最小值,接下来就需要在[mid+1,right] 这个区间进行查找,又因为mid+1指向的数字可能是我们所需要的答案,所以left的移动方式为:left=mid+1

需要注意的是如果以最左边的值作为参考值的话,当数组旋转为原数组时,会使mid一直数字更大的位置移动,最终找到最大值,所以以最左边的数作为参照物就存在边界情况。left和right最终会指向最右边的值,我们无法得知left和right最终会指向最右边的值是因为最右边的数就是最小值还是因为数组旋转为原数组,所以如果以最左边的值作为参考值的话需要进行判断,如果最右边的数小于最左边的数就说明left和right指向的就是最小的数,反之则是数组旋转为原数组,那么数组中下标为0的数就是最小的数。

编写代码

// 以最右边的元素作为参照数
// 最小元素的右边全部比参照数大
// 最小元素的左边全部比参照数小
class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0 , right = nums.size() - 1;
        int flag = right;

        while(left < right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid] > nums[flag])
                left = mid + 1;
            else
                right = mid;
        }

        return nums[left];
    }
};
// 以最左边的元素作为参照数
// 最小元素的右边全部比参照数大
// 最小元素的左边全部比参照数小
class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0 , right = nums.size() - 1;
        int flag= 0;

        while(left < right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid] >= nums[flag])
                left = mid + 1;
            else
                right = mid;
        }

        // 若数组中元素为升序,需要处理边界问题
        if(nums[left] < nums[0])
            return nums[left];
        else
            return nums[0];
    }
};

八、点名

题目描述
在这里插入图片描述

思路讲解
这道题有很多种解决方案,例如哈希表,直接遍历得到结果,位运算,高斯求和与二分查找,由于本篇文章是二分查找的专题,这里只对二分查找的方法进行讲解。

通过对题目的解析可以发现,缺席的同学会出现下图两种情况,缺席同学号是[0,n-2]中的其中一个或是缺席同学的学号是n-1,暴力解法很简单这里不过多讲解。

通过对下图的观察我们发现,这些数据有很明显的二段性,所以本题可以使用二分查找来解决,我们可以将数组分为两段,一段是下标与学号相等的左区域,一段是下标与学号不相等的右区域。
在这里插入图片描述

对数组进行划分后,我们可以知道我们查找缺席同学的学号就是第一个学号与下标不相等的下标。

接下来我们对left和right的移动方式进行分类讨论:
①当arr[i]==i时,我们可以得知mid指向的是学号等于下标的区间,这个区间内不可能出现我们所需要的答案,接下来就需要在右边的区间进行查找,又因为mid指向的数字不可能是我们所需要的答案,那么left可以直接跳过mid,所以left的移动方式为:left=mid+1
②当arr[i]=i时,我们可以得知mid指向的是学号不等于下标的区间,这个区间内可能会出现我们所需要的答案,接下来就需要在左边的区间进行查找,但是mid指向的数字可能是我们所需要的答案,那么right不能直接跳过mid,所以right的移动方式为:right=mid

对上面left和right的移动方式进行讨论后,我们就可以套用模版进行代码的编写了,mid如何计算,我们只需要记得,在left和right的移动操作中有-1,那么mid计算时就要多一个+1,具体可以看上面的模版总结。但是上面的操作只能解决缺席同学的学号在数组[0,n-2]中的情况,若是缺席同学的学号是n-1时,left和right最终指向的位置是数组的最后一个位置,我们无法知道left和right指向最后一个位置是因为数组中所有的学号与下标相同还是因为最后一个位置的下标刚好是第一个与学号不相同的下标,所以left和right指向数组中最后一个位置的时候,需要进行判断,相同则返回最后一个位置的下一个位置的下标,不相同则返回最后一个位置的下标。

编写代码

class Solution {
public:
    int takeAttendance(vector<int>& records) {
        int left = 0 , right = records.size() - 1;

        while(left < right)
        {
            int mid = left + (right - left) / 2;
            if(mid == records[mid])
                left = mid + 1;
            else
                right = mid;
        }
        
        // 处理最后缺最后一个元素
        if(left != records[left])
            return left;
        else 
            return left + 1;
    }
};

结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹

在这里插入图片描述

;