【力扣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 间接找右边界,避免单独处理

相关推荐
GuWenyue1 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
我叫洋洋3 小时前
C ++ [ hello world ]
c语言·c++·算法
奋发向前wcx4 小时前
y1,y2总复习笔记5 2026.7.19
数据结构·笔记·算法
战族狼魂7 小时前
高频面试题精选:分治与AI Agent架构
人工智能·算法·大模型·大语言模型
李剑一7 小时前
你用过网易的将军令嘛?它底层实现账号保护的原理相当简单粗暴
算法
Scott9999HH7 小时前
告别流量波动玄学!从法拉第电磁定律到底层 C/C++ 流量累积算法,深度解密工业级电磁流量计选型与开发
c语言·c++·算法
中达瑞和-高光谱·多光谱8 小时前
1nm到8nm光谱分辨率,你的应用该选哪款光谱相机?
算法·高光谱·高光谱相机
ChaoZiLL8 小时前
我的数据结构4-栈和队列
数据结构
香辣牛肉饭8 小时前
【算法】动态规划 最长公共子序列(LCS)
经验分享·笔记·算法·动态规划
miller-tsunami9 小时前
顺序表相关知识点
数据结构·顺序表