Leetcode 86 【二分查找】在排序数组中查找元素的第一个和最后一个位置

二分搜索算法核心代码模板 | labuladong 的算法笔记

1 温故而知新

题目

704. 二分查找

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

你必须编写一个具有 O(log n) 时间复杂度的算法。
示例 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

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。

代码实现

重新写的时候犯了个小错误,贴上解答错误的代码

cpp 复制代码
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0 ;
        int right = nums.size() -1 ;

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

错误:循环条件,left <= right ,两个可以相等,对于数组内只有单个元素的考虑(否则会错误的跳出循环返回 -1)

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

框架伪码

cpp 复制代码
int binarySearch(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;

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

题目

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

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

复制代码
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

复制代码
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

复制代码
输入:nums = [], target = 0
输出:[-1,-1]

代码实现

这个题目怎么觉得有点像滑动窗口,直接看看以前提交的代码回顾一下。

cpp 复制代码
using namespace std ;
class Solution {
public:
    int left_bound(vector<int>& nums ,int target){
        int left = 0 , right = nums.size() - 1 ;
        while (left <= right ){
            int mid = left + (right - left ) / 2 ;
            if (nums[mid] < target ){
                left = mid + 1 ;
            }else if (nums[mid] > target){
                right = mid - 1 ;
            }else if (nums[mid] == target){
                right = mid - 1 ;
            }
        }
        if (left < 0 || left >= nums.size() || nums[left] != target){
            return -1 ;
        }
        return left ;
    }
    int right_bound(vector<int>& nums ,int target){
        int left = 0 , right = nums.size() - 1 ;
        while (left <= right ){
            int mid = left + (right - left ) / 2 ;
            if (nums[mid] < target ){
                left = mid + 1 ;
            }else if (nums[mid] > target){
                right = mid - 1 ;
            }else if (nums[mid] == target){
                left = mid + 1 ;
            }
        }
        if (right < 0 || right >= nums.size() || nums[right] != target){
            return -1 ;
        }
        return right ;
    }


    vector<int> searchRange(vector<int>& nums, int target) {
        int left = left_bound(nums,target);
        int right = right_bound(nums,target);
        return{
            left , right
        };
    }
};

我知道了,一看这个题解,我知道我当时根本不会这个题目,我先记下来的。

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        // 直接调用原名称的左右边界查找函数
        int left = left_bound(nums, target);
        int right = right_bound(nums, target);
        return {left, right};
    }

private:
   // 查找左边界
    int left_bound(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] == target) {
                right = mid - 1; // 锁定左侧边界
            }
        }
        // 越界或值不匹配则返回-1
        if (left < 0 || left >= nums.size() || nums[left] != target) {
            return -1;
        }
        return left;
    }

    // 查找右边界
    int right_bound(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] == target) {
                left = mid + 1; // 锁定右侧边界
            }
        }
        // 越界或值不匹配则返回-1
        if (right < 0 || right >= nums.size() || nums[right] != target) {
            return -1;
        }
        return right;
    }
};

其实很清晰了,就是在做一个左边界查找和右边界查找索引的工作,根本没那么难。

自己动手写的错的代码

cpp 复制代码
class Solution {
private:
    int left_bound (vector<int>& nums, int target){
        int left = 0 ;
        int right = nums.size() -1 ;
        while(right >= left){
            int mid = left + (right - left) / 2 ;
            if(nums[mid] == target){
                right = mid - 1;
            }else if (nums[mid] > target){
                right = mid - 1;
            }else{
                left = mid +1 ;
            }
        }
        if (left < 0 || left >= nums.size() || nums[left] != target) {
            return -1;
        }
        return left;
    }
    int right_bound (vector<int>& nums, int target){
        int left = 0 ;
        int right = nums.size() -1;
        while(right >= left){
            int mid = left + (right - left) / 2 ;
            if(nums[mid] == target){
                left = mid +1 ;
            }else if (nums[mid] > target){
                right = mid - 1;
            }else{
                left = mid +1 ;
            }
        }
         if (left < 0 || left >= nums.size() || nums[left] != target) {
            return -1;
        }
        return right;
    }

public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left = left_bound(nums,target);
        int right = right_bound(nums,target);
        return{
            left, right
        };
    }
};

错误注释

cpp 复制代码
class Solution {
private:
    int left_bound (vector<int>& nums, int target){
        int left = 0 ;
        int right = nums.size() -1 ;
        while(right >= left){
            int mid = left + (right - left) / 2 ;
            if(nums[mid] == target){
                right = mid - 1;
            }else if (nums[mid] > target){
                right = mid - 1;
            }else{
                left = mid +1 ;
            }
        }
        // 错误1(非致命):left初始为0且只增不减,不可能<0,这个判断多余
        if (left < 0 || left >= nums.size() || nums[left] != target) {
            return -1;
        }
        return left; // left_bound的返回值是正确的,这里没问题
    }
    int right_bound (vector<int>& nums, int target){
        int left = 0 ;
        int right = nums.size() -1;
        while(right >= left){
            int mid = left + (right - left) / 2 ;
            if(nums[mid] == target){
                left = mid +1 ;
            }else if (nums[mid] > target){
                right = mid - 1;
            }else{
                left = mid +1 ;
            }
        }
        // 错误2(致命):校验的是left,但right_bound的结果存在right里,left此时可能越界
        // 比如nums=[1,2,2,3], target=2,循环结束后left=3,nums[3]=3≠2,直接错误返回-1
        // 正确应该校验right的合法性和值
        if (left < 0 || left >= nums.size() || nums[left] != target) {
            return -1;
        }
        // 这里return right是正确的,但上面的校验逻辑错了,导致根本走不到这一步
        return right;
    }

public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left = left_bound(nums,target);
        int right = right_bound(nums,target);
        return{
            left, right
        };
    }
};

修正后

cpp 复制代码
class Solution {
private:
    int left_bound (vector<int>& nums, int target){
        int left = 0 ;
        int right = nums.size() -1 ;
        while(right >= left){
            int mid = left + (right - left) / 2 ;
            if(nums[mid] == target){
                right = mid - 1;
            }else if (nums[mid] > target){
                right = mid - 1;
            }else{
                left = mid +1 ;
            }
        }
        if (left >= nums.size() || nums[left] != target) {
            return -1;
        }
        return left;
    }
    int right_bound (vector<int>& nums, int target){
        int left = 0 ;
        int right = nums.size() -1;
        while(right >= left){
            int mid = left + (right - left) / 2 ;
            if(nums[mid] == target){
                left = mid +1 ;
            }else if (nums[mid] > target){
                right = mid - 1;
            }else{
                left = mid +1 ;
            }
        }
         if (right>= nums.size() || nums[right] != target) {
            return -1;
        }
        return right;
    }

public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left = left_bound(nums,target);
        int right = right_bound(nums,target);
        return{
            left, right
        };
    }
};

承认偷懒了,校验逻辑这里直接复制粘贴了,没有自己再好好核对一遍。

2 二分查找归类

第一个,最基本的二分查找算法

复制代码
因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1

因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回

第二个,寻找左侧边界的二分查找

复制代码
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid

因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界

第三个,寻找右侧边界的二分查找

复制代码
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid

因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界

又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一

三类代码合起来看

cpp 复制代码
int binary_search(vector<int>& nums, int target) {
    int left = 0, right = nums.size()-1; 
    while(left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1; 
        } else if(nums[mid] == target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}

int left_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size()-1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
    // 判断 target 是否存在于 nums 中
    if (left < 0 || left >= nums.size()) {
        return -1;
    }
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
}

int right_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size()-1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1;
        }
    }
    // 由于 while 的结束条件是 right == left - 1,且现在在求右边界
    // 所以用 right 替代 left - 1 更好记
    if (right < 0 || right >= nums.size()) {
        return -1;
    }
    return nums[right] == target ? right : -1;
}

3 总结

一开始,看上面的题目还是懵懵懂懂,有很多语法的细节没有自己仔细注意。

现在回头看,无非就是3种情况,对应上面的三种逻辑:

1.查找元素

2.查找左边界

3.查找右边界

------------

虽然自己写还是会有点小问题,但是排查能力和自己修正的能力在不断提高,总比一开始什么也看不懂好的多。

我在进步,和纵向的自己比,不要浮躁,不要焦虑,慢慢来,比较快。

好巧,看了这题的初次提交时间刚好是1个月以前。

1个月从完全没思路到自己能理出框架和思路,至少有深一些的理解,我在进步,不要放弃自己,加油。

加油加油!

相关推荐
有一个好名字2 小时前
力扣:种花问题
算法·leetcode·职场和发展
会编程是什么感觉...2 小时前
算法 - Impedance Track 阻抗跟踪笔记
算法·bms·电量计
sprintzer2 小时前
力扣12.16-12.25数组刷题
算法·leetcode·职场和发展
点云侠2 小时前
基于选权迭代法的空间平面拟合
线性代数·算法·平面
小鸡吃米…2 小时前
基于Python监督学习的人工智能:分类
人工智能·python·学习
DisonTangor2 小时前
英伟达开源会打游戏的Agent模型,模仿学习人类游戏视频进行训练
学习·游戏
AndrewHZ2 小时前
【图像处理基石】VR的眩晕感是如何产生的?
图像处理·算法·计算机视觉·vr·cv·立体视觉·眩晕感
diegoXie2 小时前
【R】新手向:renv 攻克笔记
开发语言·笔记·r语言
智算菩萨2 小时前
【Python基础】排序算法的深度解析与实践应用:从理论到性能优化的全面指南
算法·性能优化·排序算法