【力扣100题】78.在排序数组中查找元素的第一个和最后一个位置

题目描述

给你一个按照非递减顺序排列的整数数组 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]

解题思路总览

方法 核心思想 时间复杂度 空间复杂度 特点
二分查找(左边界 + 右边界) 分别找 target 的左边界和右边界 O(log n) O(1) 标准解法,最常考
二分查找(lower_bound 变形) 利用 lower_bound 找左边界,再减1找右边界 O(log n) O(1) 代码简洁
直接遍历(不推荐) 线性扫描数组 O(n) O(1) 不满足题目要求,仅作对比

方法一:二分查找(左边界 + 右边界)

代码实现

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.empty()) return {-1, -1};
        
        // 找左边界(第一个 >= target 的位置)
        int start = findLowerBound(nums, target);
        
        // 检查左边界是否有效
        if (start == nums.size() || nums[start] != target) {
            return {-1, -1};
        }
        
        // 找右边界(第一个 > target 的位置,然后减1)
        int end = findLowerBound(nums, target + 1) - 1;
        
        return {start, end};
    }
    
private:
    int findLowerBound(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) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
};

核心思想

本题本质是查找 target 的左边界和右边界:

  1. 左边界:第一个 >= target 的位置
  2. 右边界:第一个 > target 的位置减1

利用二分查找的变体,通过 nums[mid] >= target 来收缩右边界,逐步逼近左边界。

算法流程图

复制代码
以 nums = [5,7,7,8,8,10], target = 8 为例:

第一步:找左边界(第一个 >= 8 的位置)
  初始:left=0, right=5
  第1轮:mid=2, nums[2]=7 < 8,left=mid+1=3
  第2轮:mid=4, nums[4]=8 >= 8,right=mid-1=3
  第3轮:mid=3, nums[3]=8 >= 8,right=mid-1=2
  left=3, right=2,循环结束
  返回 left = 3(左边界)

第二步:找右边界(第一个 > 8 的位置减1)
  找 > 8,实际上就是找第一个 >= 9 的位置
  调用 findLowerBound(nums, 9)
  初始:left=0, right=5
  第1轮:mid=2, nums[2]=7 < 9,left=mid+1=3
  第2轮:mid=4, nums[4]=8 < 9,left=mid+1=5
  第3轮:mid=5, nums[5]=10 >= 9,right=mid-1=4
  left=5, right=4,循环结束
  返回 left = 5,然后 end = 5 - 1 = 4(右边界)

结果:[3, 4]

逐行解析

cpp 复制代码
if (nums.empty()) return {-1, -1};

处理空数组边界情况,直接返回 -1, -1


cpp 复制代码
int start = findLowerBound(nums, target);

调用辅助函数找到 target 的左边界(第一个 >= target 的位置)。


cpp 复制代码
if (start == nums.size() || nums[start] != target) {
    return {-1, -1};
}

两种情况说明 target 不存在于数组中:

  1. start == nums.size():说明所有元素都小于 target
  2. numsstart != target:说明 start 位置的值大于 target(因为 start 是第一个 >= target 的位置)

cpp 复制代码
int end = findLowerBound(nums, target + 1) - 1;

找右边界:第一个 > target 的位置 = 第一个 >= target+1 的位置,减 1 得到右边界。

为什么要用 target+1?因为数组是整数,可以通过找 target+1 的左边界来间接找到 > target 的位置。


cpp 复制代码
int findLowerBound(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) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

找左边界(第一个 >= target 的位置)的核心逻辑:

  • numsmid >= target:说明左边界在 left, mid 区间(包括 mid),更新 right = mid - 1
  • numsmid < target:说明左边界在 mid+1, right 区间,更新 left = mid + 1

循环结束后,left 就是第一个 >= target 的位置。

复杂度分析

复杂度 分析
时间 两次二分查找,每次 O(log n),总共 O(log n)
空间 O(1),只使用了常数个变量

方法二:直接使用 lower_bound(STL 思想)

代码实现

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.empty()) return {-1, -1};
        
        // 使用 STL lower_bound 找左边界
        auto lower_it = lower_bound(nums.begin(), nums.end(), target);
        
        // 检查左边界是否有效
        if (lower_it == nums.end() || *lower_it != target) {
            return {-1, -1};
        }
        
        // 使用 STL upper_bound 找右边界
        auto upper_it = upper_bound(nums.begin(), nums.end(), target);
        
        int start = lower_it - nums.begin();
        int end = upper_it - nums.begin() - 1;
        
        return {start, end};
    }
};

核心思想

直接利用 C++ STL 的两个标准库函数:

  • lower_bound:返回第一个 >= target 的迭代器
  • upper_bound:返回第一个 > target 的迭代器

STL 原理简析

lower_bound 的实现逻辑与手动实现完全相同:

cpp 复制代码
template<class ForwardIt, class T>
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value) {
    ForwardIt it;
    while (first < last) {
        it = first + (last - first) / 2;
        if (*it < value) {
            first = it + 1;
        } else {
            last = it;
        }
    }
    return first;
}

复杂度分析

复杂度 分析
时间 O(log n)
空间 O(1)

方法三:手动实现 lower_bound 和 upper_bound

代码实现

cpp 复制代码
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.empty()) return {-1, -1};
        
        int start = myLowerBound(nums, target);
        if (start == nums.size() || nums[start] != target) {
            return {-1, -1};
        }
        
        int end = myUpperBound(nums, target) - 1;
        return {start, end};
    }
    
private:
    int myLowerBound(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 {
                right = mid - 1;
            }
        }
        return left;
    }
    
    int myUpperBound(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 {
                right = mid - 1;
            }
        }
        return left;
    }
};

myLowerBound vs myUpperBound 对比

对比项 myLowerBound myUpperBound
目标 第一个 >= target 第一个 > target
条件 numsmid < target numsmid <= target
等于时的动作 right = mid - 1 left = mid + 1

算法流程图

复制代码
myLowerBound(nums, target):
  目标:找第一个 >= target 的位置
  条件:nums[mid] < target -> left = mid + 1
        否则 -> right = mid - 1

myUpperBound(nums, target):
  目标:找第一个 > target 的位置
  条件:nums[mid] <= target -> left = mid + 1
        否则 -> right = mid - 1

以 nums = [5,7,7,8,8,10], target = 8 为例:

myLowerBound(nums, 8):
  找第一个 >= 8,返回 3

myUpperBound(nums, 8):
  找第一个 > 8,返回 5
  end = 5 - 1 = 4

结果:[3, 4]

复杂度分析

复杂度 分析
时间 O(log n)
空间 O(1)

边界情况分析

情况1:数组为空

复制代码
输入: nums = [], target = 0
分析: nums.empty() 为 true,直接 return {-1, -1}
结果: [-1, -1]

情况2:target 小于所有元素

复制代码
输入: nums = [5,7,7,8,8,10], target = 3
分析: 找左边界 findLowerBound(nums, 3)
     逐步向右移动,最终 left = 0
     但 nums[0] = 5 != 3
结果: [-1, -1]

情况3:target 大于所有元素

复制代码
输入: nums = [5,7,7,8,8,10], target = 100
分析: 找左边界 findLowerBound(nums, 100)
     逐步向左移动,最终 left = 6 = nums.size()
     start == nums.size()
结果: [-1, -1]

情况4:target 只出现一次

复制代码
输入: nums = [5,7,7,8,8,10], target = 10
分析: 左边界:找到 10 在位置 5
     右边界:找第一个 > 10,即第一个 >= 11
     找不到,upper = 6,end = 6 - 1 = 5
结果: [5, 5]

情况5:target 充满整个数组

复制代码
输入: nums = [8,8,8,8], target = 8
分析: 左边界:findLowerBound(nums, 8) = 0
     右边界:findLowerBound(nums, 9) - 1 = 4 - 1 = 3
结果: [0, 3]

情况6:target 不存在但夹在两个数之间

复制代码
输入: nums = [5,7,7,8,8,10], target = 9
分析: 左边界:findLowerBound(nums, 9)
     第1轮:mid=2, nums[2]=7 < 9,left=3
     第2轮:mid=4, nums[4]=8 < 9,left=5
     第3轮:mid=5, nums[5]=10 >= 9,right=4
     left=5, right=4,循环结束
     返回 left = 5,但 nums[5] = 10 != 9
结果: [-1, -1]

面试追问 FAQ

问题 回答
为什么要用 target+1 来找右边界? 数组是整数,可以通过找 target+1 的左边界来间接找到 > target 的位置。这是避免单独写 upper_bound 的技巧
left <= right 和 left < right 有什么区别? left <= right 最终 left > right;left < right 最终 left == right。两者都可用于二分,区别在于初始边界设置
如果数组中有重复元素怎么办? 两种方法都能正确处理:方法一用 target+1 间接找右边界;方法三直接用 upper_bound
如何处理负数 target? target+1 可能溢出(如果 target = INT_MAX)。解决方案:直接用 upper_bound 或改用 target + 1LL
二分查找的常见陷阱有哪些? 1. 整数溢出:用 mid = left + (right - left) / 2 代替 mid = (left + right) / 2;2. 循环终止条件:left <= right 还是 left < right;3. 边界更新:left = mid + 1 还是 left = mid
如果要求时间复杂度为 O(log n),为什么不能遍历? 遍历最坏情况 O(n),不满足要求。二分查找每次将搜索区间缩小一半,保证 O(log n)
如何在 O(log n) 内同时找左边界和右边界? 思路一:两次二分(分别找左边界和右边界);思路二:一次二分找到任意一个 target,再向左右扩展(最坏 O(n))

相关题目

题目 难度 核心区别
34. 在排序数组中查找元素的第一个和最后一个位置(本题) 困难 找左边界和右边界
35. 搜索插入位置 简单 找插入位置(相当于左边界)
74. 搜索二维矩阵 中等 二维矩阵的二分查找
704. 二分查找 简单 标准二分查找
278. 第一个错误的版本 简单 二分查找的简单应用
162. 寻找峰值 中等 二分查找找峰值

总结

要点 说明
核心思想 利用数组有序,通过二分查找找左边界(第一个 >= target)和右边界(第一个 > target)
找左边界 第一个 >= target 的位置
找右边界 第一个 > target 的位置 = 第一个 >= target+1 的位置减 1
防溢出 使用 mid = left + (right - left) / 2
时间复杂度 O(log n)
空间复杂度 O(1)
关键技巧 用 target+1 间接找右边界,避免单独处理

相关推荐
江屿风1 小时前
C++图论基础拓扑排序算法流食般投喂
开发语言·c++·笔记·算法·排序算法
海棠AI实验室2 小时前
AI 时代文献综述:从检索到成稿的 RAG 五步法
windows·算法·自动化·llm·rag
H178535090962 小时前
SolidWorks_基于草图的实体特征14_扫描扭转与控制
前端·人工智能·算法·3d建模·solidworks
黄金龙PLUS2 小时前
基于ARX结构的新型序列密码算法FlashLight
算法·网络安全·密码学·哈希算法·同态加密
meilindehuzi_a2 小时前
深入理解JavaScript线性数据结构:从内存视角探究数组、链表、栈与队列
javascript·数据结构·链表
洛水水2 小时前
【力扣100题】77.搜索二维矩阵
算法·leetcode·矩阵
m0_547486662 小时前
华南农业大学《数据结构》期末试卷及答案2011-2019 2020-2023年PDF
大数据·数据结构·pdf·华南农业大学
仙俊红2 小时前
深入理解 ThreadLocal —— 从变量引用、强弱引用到 Spring Boot 实战
spring boot·python·算法
故渊at2 小时前
第五板块:Android 系统服务与电源管理 | 第十八篇:Battery Service 与 电量统计(Fuel Gauge)算法
android·算法·battery·电源·电池·电源管理·电量统计