【算法】Leetcode第34题在排序数组中查找元素的第一个和最后一个位置(二分查找的特殊修改)

6 篇文章 0 订阅

二分查找的一般模板如下所示,首先边界取的是0和n-1,左边界和右边界都可以取值,所以while的条件语句是left小于等于right。这个一般模板适用的是无重复的有序数组。

        int left = 0;
        int n = nums.size();
        int right = n - 1;
        while (left <= right) {
            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;

像第34题这种,找到target的开始位置和结束位置,需要对二分查找做出特殊的修改。
在这里插入图片描述
首先我们要定义两个函数,一个查找开始位置,一个查找结束位置。

查找开始位置函数如下所示:


    int findFirst(vector<int>& nums, int target) {
        int left = 0;
        int n = nums.size();
        int right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;    //找中间值
            if (nums[mid] > target)
                right = mid - 1;
            else if (nums[mid] < target)
                left = mid + 1;
            else 
                right = mid;
        }
        if (nums[left] == target)
            return left;
        else    
            return -1;
    }

查找开始位置的二分查找和普通的模板区别在于:

  • while判断条件变了,符合条件left < right的继续进行下一轮循环,从while挑出来的时候一定是不符合条件,left一定是等于right。
  • 当找到nums[mid]等于target的时候,可以肯定的是在mid右边的编号不是我们想要的开始位置,但是mid也不一定是开始位置,可能左边还有等于target的数,所以需要继续往左边找。如果mid左边并不存在其他等于taget的值,那mid就是开始位置,所以改变边界的时候,不能把mid排除在外,right = mid。下面我们看看这一点小小的改变是如何帮助我们找到开始位置的。

考虑下面的情况,target = 8,此时nums[mid] = target,下一步就应该在left和mid之间寻找开始位置。
在这里插入图片描述
计算得到新的mid,nums[1] < target,left指针一直往右走,直到指向nums[2]的位置,此时nums[mid] == target, 即符合第三个if语句,又所以right等于mid, 向左移动,此时left == right跳出while循环,就找到了taget第一次出现的位置。
在这里插入图片描述
考虑另外一种情况,mid已经是target的开始位置,前面已经没有其他等于target的数,计算新的mid,并一直把left指针往右移动。当处于如下图的情况时时,发现nums[mid] < target,left指针继续右移,此时left == right且都指向taget,跳出while循环。
在这里插入图片描述
当我们跳出while循环之后,要对left或right所指向的数进行判断(此时left和right相等),因为有可能这个数组根本不存在target。如果最终left指针指向的数等于target,不管是返回left,还是返回right,都是可以的。如果这个数不等于target,那说明数组内根本不存在等于target的数,返回-1;

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

以下是寻找结束位置的函数:

int findEnd(vector<int>& nums, int target) {
        int left = 0;
        int n = nums.size();
        int right = n - 1;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;    //找中间值
            if (nums[mid] > target)
                right = mid - 1;
            else if (nums[mid] < target)
                left = mid + 1;
            else 
                left = mid;
        }
        return left;
    }

寻找结束位置和寻找开始位置最大的不同,在于mid的取值不一样。这是很重要的一点,如果这个没有修改过来,就会陷入死循环。

这个mid取值加不加1,其实对于数量为奇数的数组,是没有影响的。比如对于n = 3的数组,如果没有加1,left = 0, right = 2, mid = 0 + (2 - 0) / 2 = 1,此时mid计算出来是1,刚好是中间那个编号。 接下来看看加1的做法,left = 0, right = 2, mid = 0 + (2 - 0 + 1) / 2 = 1,会发现后面的3 / 2 = 1.5被隐式转换成整数1,最终计算出来的结果还是1。

接下来看看数量为偶数的数组,对于n = 4的数组,中间其实是有两个编号1和2的。如果没有加1,left = 0, right = 3, mid = 0 + (3- 0) / 2 = 1,此时mid计算出来是1,是中间两个取左值。 接下来看看加1的做法,left = 0, right = 3, mid = 0 + (3 - 0 + 1) / 2 = 2,此时mid计算出来是2,是中间两个取右值。

所以int mid = left + (right - left + 1) / 2这里加1是为了偶数数量的数组在取mid的时候,取到中间数组偏右的那个,那为什么要这么取呢?前面寻找开始位置的时候,明明取的是左值。

考虑下面这种情况,此时nums[mid] == target,我们是想继续看看右边的数字的,我们令left = mid,然后继续计算[left, right]的中间值,然后算出来mid取左值,即等于left(又是原来那个数),然后对nums[mid]进行判断,发现相等,令left = mid,继续计算mid,此时就会陷入无限循环。
在这里插入图片描述
所以我们要让int mid = left + (right - left + 1) / 2取到右值,即下图的情况。此时nums[mid] = target,所以令left = mid,此时left和right相等,退出while循环。
在这里插入图片描述
总结:只要二分查找类型的算法出现left = mid的时候,记得取mid取中间的右值。

完整代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        if (n == 0) return {-1, -1};
        int start = findFirst(nums, target);
        //如果返回-1,证明没找到target,那也不用继续找结束位置了
        if (start == -1) return {-1, -1};
        int end = findEnd(nums, target);

        
        return {start, end};
    }

    int findFirst(vector<int>& nums, int target) {
        int left = 0;
        int n = nums.size();
        int right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;    //找中间值
            if (nums[mid] > target)
                right = mid - 1;
            else if (nums[mid] < target)
                left = mid + 1;
            else 
                right = mid;
        }
        if (nums[left] == target)
            return left;
        else    
            return -1;
    }

    int findEnd(vector<int>& nums, int target) {
        int left = 0;
        int n = nums.size();
        int right = n - 1;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;    //找中间值
            if (nums[mid] > target)
                right = mid - 1;
            else if (nums[mid] < target)
                left = mid + 1;
            else 
                left = mid;
        }
        return left;
    }
};
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值