每天一道LeetCode-----某个数在递增序列第一次和最后一次出现的位置

这篇博客介绍了如何使用二分法解决LeetCode中的Search for a Range问题,即在一个递增序列中找到指定值的首次和最后一次出现的位置。文章详细解析了寻找左右边界的二分查找过程,并提出了在寻找右边界时防止无限循环的解决方案。

Search for a Range

原题链接Search for a Range
这里写图片描述
给定一个递增序列和一个值,找到该值在序列中出现的范围,实际上就是找到该值第一次出现和最后一次出现的位置。如果没有,返回[-1,-1]


递增序列肯定是二分了,正常二分法查找算法如下,是通过判断中间位置的值与给定值的大小关系,从而将区间变为原来的一半,继续查找,不断的一半,一半,最后变成只有一个元素的区间,比较后返回。

int binary_find(vector<int>& nums, int target)
{
    int left = 0;
    int right = nums.size();
    while(left <= right)
    {
        int middle = (left + right) / 2;
        if(nums[middle] == target)
            return middle;
        else if(nums[middle] > target)
            right = middle - 1;
        else
            left = middle + 1;
    }

    return -1;
}

普通的二分法查找到一个相等的值就结束了,但是这里需要确定这个值第一次出现和最后一次出现的位置。所以很明显不能让它结束这么快,也就是说即使nums[middle] == target,也不返回,因为目的是要找一个范围,即两个边界,而middle只是一个点,不一定是边界,有可能middle前面和后面也都是等于target的位置。
但是又因为二分法最后肯定会收敛到一个点,不能直接找范围,所以可以先找左边界,再找右边界


二分法需要保证,如果序列中存在目标元素target,那么最后收敛到的位置的值一定是target

对于左边界
考虑二分法的实现,每次找到middle后比较nums[middle]和target的大小关系

  • 如果nums[middle] > target,说明target在[left, middle)区间,改变right = middle - 1;
  • 如果nums[middle] < target,说明target在(middle, right]区间,改变left = middle + 1;
  • 如果nums[middle] == target,说明第一次出现target的位置在[left, middle]内,改变right = middle。因为middle位置有可能就是第一次出现target的位置,所以不能让right = middle - 1;

对于右边界
考虑二分法的实现,每次找到middle后比较nums[middle]和target的大小关系

  • 如果nums[middle] > target,说明target在[left, middle)区间,改变right = middle - 1;
  • 如果nums[middle] < target,说明target在(middle, right]区间,改变left = middle + 1;
  • 如果nums[middle] == target,说明最后一次出现target的位置在[middle, right]内,改变left = middle。因为middle位置有可能就是最后一次出现target的位置,所以不能让right = middle - 1;

但是!考虑一种情况,求右边界时,某次区间[left, right]长度只有2,也就是说right = left + 1,这就导致middle = left。如果nums[middle] == target,根据上面的式子,另left = middle,此时left根本没有变化,也就是说改变left后区间根本没有更新,会陷入无限循环

这种问题只出现在求右边界的情况,原因是在求左边界时,right不可能和middle相等,所以每次区间都会变小,不会出现上面的问题

怎么解决呢,可以当区间长度为2时手动判断nums[left]和nums[right]。因为目的是求最后一个target出现的位置,而right的位置是区间的最右边,所以如果nums[right] == target,那么根本就不需要再找了,right就是最后一次出现target的位置 ,而如果nums[right] != target,那么right -= 1将right左移。

代码如下

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.empty())
            return {-1, -1};
        int front = equalLeftBound(nums, target);
        int back = equalRightBound(nums, target);
        if(front < nums.size() && back >= 0 && nums[front] == target && nums[back] == target)
            return {front, back};
        else
            return {-1, -1};
    }
private:
    /* 寻找第一次出现target的位置 */
    int equalLeftBound(vector<int>& nums, int target)
    {
        int left = 0;
        int right = nums.size() - 1;
        while(left < right)
        {
            int middle = (left + right) / 2;
            /*
             * if(nums[middle] < target)
             *     left = middle + 1;
             * else if(nums[middle] > target)
             *     right = middle - 1;
             * else
             *     right = middle;
             */


            /* 
             * 这里将nums[middle] > target和nums[middle] == target合在一起
             * 对于left是否需要加一可以通过nums[middle]是否等于target判断
             * 因为right永远不会和middle相等,所以区间会一直减小,不会出现无限循环
             * 怎么写无所无,但是右边界不行
             */
            if(nums[middle] < target)
                left = middle + 1;
            else
                right = middle;

            /* 
             * 如果nums[middle] < target导致left = middle + 1后
             * nums[left]仍然小于target会导致left继续加一,这里可能出现left > right的情况
             * 如果此时right = nums.size() - 1,那么left就越界了
             * 返回的left也就越界了,需要在返回后判断
             */
            if(nums[left] == target)
                break;
            else
                ++left;
        }

        return left;
    }

    /* 寻找最后一次出现target的位置 */
    int equalRightBound(vector<int>& nums, int target)
    {
        int left = 0;
        int right = nums.size() - 1;
        while(left < right)
        {
            /* 
             * 这里只能合在一起,因为left可能和middle相等,导致区间根本没有更新,导致无限循环
             * 所以需要改变区间,从而将区间缩小
             */
            int middle = (left + right) / 2;
            if(nums[middle] > target)
                right = middle - 1;
            else
                left = middle;

            /* 如果右边界就是target,直接返回即可,否则需要将right减小,因为最后的结果是right */
            /* 
             * 同理左边界,如果nums[middle] > target导致right = middle - 1
             * 而nums[right]仍然大于target,会导致right继续减一,可能出现right < left的情况
             * 如果此时left = 0,那么right就越界了,需要在返回后判断是否越界
             */
            if(nums[right] == target)
                break;
            else
                --right;
        }

        return right;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值