LeetCode 34排序数组中查找元素的第一个和最后一个位置-二分查找

目录

[LeetCode 34:排序数组中查找元素的第一个和最后一个位置](#LeetCode 34:排序数组中查找元素的第一个和最后一个位置)

一、题目描述

二、核心思路:二分法找「边界」

三、关键转化逻辑

四、lowerBound函数详解

五、完整代码实现(Java)

六、示例流程拆解

七、边界情况测试

八、传统解法对比(分别找左右边界)

九、常见问题解答

[1. 为什么用target+1?会有溢出风险吗?](#1. 为什么用target+1?会有溢出风险吗?)

[2. 为什么lowerBound用左闭右开区间?](#2. 为什么lowerBound用左闭右开区间?)

[3. 为什么计算 mid 用left + (right - left)/2?](#3. 为什么计算 mid 用left + (right - left)/2?)

十、关键点

[十一、全闭区间 [left, right]](#十一、全闭区间 [left, right])

[1. 区间定义](#1. 区间定义)

[2. 代码实现](#2. 代码实现)

[3. 关键逻辑解释](#3. 关键逻辑解释)

[4. 作用 / 适用场景](#4. 作用 / 适用场景)

[十二、左开右闭区间 (left, right]](#十二、左开右闭区间 (left, right])

[1. 区间定义](#1. 区间定义)

[2. 代码实现](#2. 代码实现)

[3. 关键逻辑解释](#3. 关键逻辑解释)

[4. 作用 / 适用场景](#4. 作用 / 适用场景)

十三、全开区间 (left, right)

[1. 区间定义](#1. 区间定义)

[2. 代码实现](#2. 代码实现)

[3. 关键逻辑解释](#3. 关键逻辑解释)

[4. 作用 / 适用场景](#4. 作用 / 适用场景)

十四、各区间核心差异对比表

十五、选型建议

十六、完整验证代码


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

一、题目描述

给定一个非递减排序 的整数数组 nums 和目标值 target,请找出 target 在数组中的起始位置结束位置 。若数组中不存在 target,返回 [-1, -1]

要求:算法的时间复杂度必须为 O(log n)(暴力遍历 O (n) 不满足要求)。

示例说明

  • 示例 1:输入:nums = [5,7,7,8,8,10], target = 8输出:[3,4](8 第一次出现在索引 3,最后一次在索引 4)
  • 示例 2:输入:nums = [5,7,7,8,8,10], target = 6输出:[-1,-1](数组中无 6)
  • 示例 3:输入:nums = [1], target = 1输出:[0,0](仅一个元素且为 target)

二、核心思路:二分法找「边界」

数组是非递减有序的,这意味着:

  • 所有等于 target 的元素会连续集中在某个区间内;
  • 我们只需找到这个区间的左边界 (第一个等于 target 的位置)和右边界(最后一个等于 target 的位置)。

而找边界的关键是:将「找等于 target 的边界」转化为「找第一个≥x 的位置」 (利用lowerBound函数),从而复用同一套二分逻辑,简化代码。

找等于target的左边界---找到第一个≥target的位置

****找等于target的右边界---找到第一个≥target+1的位置,然后-1

三、关键转化逻辑

非递减数组中,「target 的起止位置」可以通过以下规则转化为lowerBound的查询:

目标位置 转化为「找第一个≥x 的位置」 示例(target=8)
起始位置(左边界) x = target 找第一个≥8 的位置 → 3
结束位置(右边界) x = target + 1 找第一个≥9 的位置 → 5,再减 1 → 4

为什么这个转化成立?

数组非递减的特性保证了:

  1. 「第一个≥target」的位置,就是 target 第一次出现的位置(若存在);
  2. 「第一个≥(target+1)」的位置,是第一个比 target 大的数的位置,它的前一个位置必然是 target 最后一次出现的位置。

四、lowerBound函数详解

lowerBound是二分法的核心工具,功能是:在非递减数组 中,找到第一个≥target的元素的索引。

实现逻辑(左闭右开区间)

采用「左闭右开区间 [left, right)」设计(右边界初始为nums.length),好处是:

  • 避免处理「最后一个元素」的边界问题;
  • 终止条件统一为left == right,无需额外判断。
java 复制代码
// 核心:找第一个≥target的元素索引(左闭右开区间)
private int lowerBound(int[] nums, int target) {
    int left = 0;
    int right = nums.length; // 左闭右开区间:[left, right)
    while (left < right) { // 区间不为空时循环
        int mid = left + (right - left) / 2; // 避免(left+right)溢出
        if (nums[mid] >= target) {
            // 目标在左半区,收缩右边界(保留mid,因为可能是第一个≥target的位置)
            right = mid;
        } else {
            // 目标在右半区,收缩左边界(mid不可能是目标,直接+1)
            left = mid + 1;
        }
    }
    return left; // 最终left==right,即第一个≥target的位置
}

注意:Java 中计算 mid 时,用left + (right - left) / 2而非(left + right) / 2,是为了避免left + right超出 int 范围导致溢出(比如 left 和 right 都是接近 Integer.MAX_VALUE 的数)。

五、完整代码实现(Java)

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 步骤1:找起始位置(第一个≥target的位置)
        int start = lowerBound(nums, target);
        
        // 验证target是否存在:
        // - start越界(所有元素都<target)
        // - start位置的元素≠target(无target)
        if (start == nums.length || nums[start] != target) {
            return new int[]{-1, -1};
        }
        
        // 步骤2:找结束位置(第一个≥(target+1)的位置 - 1)
        int end = lowerBound(nums, target + 1) - 1;
        
        return new int[]{start, end};
    }

    // 核心:找第一个≥target的元素索引(左闭右开区间)
    private int lowerBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length; // 左闭右开区间:[left, right)
        while (left < right) { // 区间不为空时循环
            int mid = left + (right - left) / 2; // 避免溢出
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
}

六、示例流程拆解

(nums=[5,7,7,8,8,10], target=8)

步骤 1:找 start = lowerBound (nums, 8)

循环次数 left right mid nums[mid] 条件(nums [mid]≥8) 调整后 left/right
1 0 6 3 8 right=3
2 0 3 1 7 left=2
3 2 3 2 7 left=3
终止 3 3 - - - 返回 3

步骤 2:找 end = lowerBound (nums, 9) - 1

先计算lowerBound(nums, 9)

循环次数 left right mid nums[mid] 条件(nums [mid]≥9) 调整后 left/right
1 0 6 3 8 left=4
2 4 6 5 10 right=5
3 4 5 4 8 left=5
终止 5 5 - - - 返回 5

因此end = 5 - 1 = 4,最终返回[3,4]

七、边界情况测试

情况 1:数组为空(nums=new int [0])

  • start = lowerBound(new int[0], target) = 0
  • 验证start == nums.length(0==0),返回[-1,-1]

情况 2:target 是第一个元素(nums=[2,2,3], target=2)

  • start = lowerBound(nums,2) = 0
  • end = lowerBound(nums,3) - 1 = 2 - 1 = 1
  • 返回[0,1]

情况 3:target 是最后一个元素(nums=[1,3,5], target=5)

  • start = lowerBound(nums,5) = 2
  • end = lowerBound(nums,6) - 1 = 3 - 1 = 2
  • 返回[2,2]

八、传统解法对比(分别找左右边界)

除了复用lowerBound,也可以分别写「找左边界」和「找右边界」的二分函数,逻辑更直观但代码稍冗余:

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int left = findLeft(nums, target);
        int right = findRight(nums, target);
        return new int[]{left, right};
    }

    // 找左边界:第一个等于target的位置
    private int findLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int res = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                res = mid;
                right = mid - 1; // 继续找更左的位置
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return res;
    }

    // 找右边界:最后一个等于target的位置
    private int findRight(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int res = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                res = mid;
                left = mid + 1; // 继续找更右的位置
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return res;
    }
}

九、常见问题解答

1. 为什么用target+1?会有溢出风险吗?

  • 转化逻辑的核心是 "找第一个比 target 大的数",target+1是最直接的方式;

  • targetint最大值(Integer.MAX_VALUE),target+1会溢出为Integer.MIN_VALUE,此时可优化逻辑:

    java 复制代码
    // 避免target+1溢出的写法
    int end = lowerBound(nums, target) == nums.length 
              ? -1 
              : lowerBound(nums, target + 1) - 1;

    实际面试 / 刷题中,targetInteger.MAX_VALUE的场景极少,上述优化仅作兜底。

2. 为什么lowerBound用左闭右开区间?

  • 右边界设为nums.length,避免了 "数组最后一个元素" 的越界判断(比如数组长度为 0 时,right=0 直接返回);
  • 终止条件left==right更简洁,无需额外处理left>right的情况;
  • 符合 Java 中数组的索引习惯(索引范围0 ~ nums.length-1,右边界设为nums.length刚好覆盖所有可能)。

3. 为什么计算 mid 用left + (right - left)/2

Java 中int的取值范围是[-2^31, 2^31-1],若leftright都接近2^31-1left + right会超出int范围导致整数溢出 ,而left + (right - left)/2能避免这个问题(等价于(left + right)/2,但无溢出风险)。

十、关键点

这道题的核心是利用二分法找「边界」 ,复用lowerBound的转化思路是 Java 版的最优写法:

  1. 起始位置 = 第一个≥target 的位置;
  2. 结束位置 = 第一个≥(target+1) 的位置 - 1;
  3. 验证 target 是否存在,不存在则返回[-1,-1]

该写法兼顾了代码简洁性和效率(时间复杂度 O (log n),空间复杂度 O (1)),符合题目要求,也是面试中面试官期望看到的最优解。


下面逐一讲解「全闭、左开右闭、全开」三种区间的代码实现、逻辑和作用,并对比差异。

十一、全闭区间 [left, right]

1. 区间定义

  • 左指针 left:初始 0(包含,数组第一个元素);
  • 右指针 right:初始 nums.length - 1(包含,数组最后一个元素);
  • 循环条件:left <= right(区间不为空,因为闭区间 left == right 时仍有一个元素);

2. 代码实现

java 复制代码
// 全闭区间 [left, right] 实现 lowerBound
private int lowerBound_Closed(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    // 初始化结果为数组长度(默认所有元素都 < target)
    int res = nums.length;
    
    while (left <= right) {
        int mid = left + (right - left) / 2; // 避免溢出
        if (nums[mid] >= target) {
            // 找到≥target的元素,记录位置,继续找更左的位置
            res = mid;
            right = mid - 1; // 收缩右边界(排除mid,找左半区)
        } else {
            // 元素<target,找右半区
            left = mid + 1;
        }
    }
    return res; // 最终res是第一个≥target的位置
}

3. 关键逻辑解释

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

循环次数 left right mid nums[mid] 条件(≥8) 调整逻辑 res
1 0 5 2 7 left=3 6
2 3 5 4 8 res=4,right=3 4
3 3 3 3 8 res=3,right=2 3
终止 3 2 - - - 返回 res=3 3

4. 作用 / 适用场景

  • 优点:逻辑最直观,符合新手对 "区间" 的认知(从 0 到最后一个元素),调试时容易跟踪指针位置;
  • 缺点:需要额外维护 res 变量(因为循环终止时 left > right,无法直接返回指针),略增加代码量;
  • 适用:新手入门、面试中快速手写(不易出错)。

十二、左开右闭区间 (left, right]

1. 区间定义

  • 左指针 left:初始 -1(不包含,数组第一个元素的左侧);
  • 右指针 right:初始 nums.length - 1(包含,数组最后一个元素);
  • 循环条件:left < right(区间不为空,开区间左指针不能等于右指针);

2. 代码实现

java 复制代码
// 左开右闭区间 (left, right] 实现 lowerBound
private int lowerBound_LeftOpenRightClosed(int[] nums, int target) {
    int left = -1;
    int right = nums.length - 1;
    
    while (left < right) {
        // 取上中位数(避免死循环):(left + right + 1) / 2
        int mid = left + (right - left + 1) / 2; 
        if (nums[mid] >= target) {
            // 目标在左半区,收缩右边界(保留mid,因为right是闭区间)
            right = mid - 1;
        } else {
            // 目标在右半区,收缩左边界(left是开区间,直接设为mid)
            left = mid;
        }
    }
    // 循环结束时left=right,第一个≥target的位置是left+1
    return left + 1;
}

3. 关键逻辑解释

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

循环次数 left right mid nums[mid] 条件(≥8) 调整逻辑
1 -1 5 2 7 left=2
2 2 5 4 8 right=3
3 2 3 3 8 right=2
终止 2 2 - - - 返回 left+1=3

4. 作用 / 适用场景

  • 优点:无需额外维护 res 变量,循环结束后通过left+1直接得到结果;
  • 缺点:左指针初始为 - 1(违反直觉),且需要取「上中位数」(mid = left + (right-left+1)/2)避免死循环,新手易出错;
  • 适用:算法竞赛中追求代码极简(少变量),但日常开发 / 面试不推荐。

十三、全开区间 (left, right)

1. 区间定义

  • 左指针 left:初始 -1(不包含,数组第一个元素左侧);
  • 右指针 right:初始 nums.length(不包含,数组最后一个元素右侧);
  • 循环条件:left + 1 < right(区间不为空,全开区间需至少隔一个元素);

2. 代码实现

java 复制代码
// 全开区间 (left, right) 实现 lowerBound
private int lowerBound_AllOpen(int[] nums, int target) {
    int left = -1;
    int right = nums.length;
    
    while (left + 1 < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) {
            // 目标在左半区,收缩右边界
            right = mid;
        } else {
            // 目标在右半区,收缩左边界
            left = mid;
        }
    }
    // 循环结束时left+1=right,right就是第一个≥target的位置
    return right;
}

3. 关键逻辑解释

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

循环次数 left right mid nums[mid] 条件(≥8) 调整逻辑
1 -1 6 2 7 left=2
2 2 6 4 8 right=4
3 2 4 3 8 right=3
终止 2 3 - - - 返回 right=3

4. 作用 / 适用场景

  • 优点:区间逻辑完全对称(左右都开),数学上更优雅;
  • 缺点:指针初始值违反直觉,循环条件是left+1 < right(新手易写错),实际应用场景极少;
  • 适用:算法理论研究 / 教学(讲解区间对称性),工程开发 / 面试几乎不用。

十四、各区间核心差异对比表

区间类型 初始化(left/right) 循环条件 指针调整核心 返回值 优点 缺点
左闭右开 [L,R) 0 / nums.length left < right right=mid / left=mid+1 left(=right) 代码极简、无死循环 右边界为 nums.length(略反直觉)
全闭 [L,R] 0 / nums.length-1 left <= right 需维护 res,right=mid-1/L=mid+1 res 逻辑最直观、新手友好 多一个 res 变量
左开右闭 (L,R] -1 / nums.length-1 left < right 取上中位数,right=mid-1/L=mid left+1 无 res 变量 初始值反直觉、易死循环
全开 (L,R) -1 / nums.length left+1 < right right=mid / left=mid right 区间对称、数学优雅 条件 / 初始值易写错

十五、选型建议

  1. 优先选左闭右开 [left, right)

    • 代码极简(无需 res 变量)、无死循环风险,是 Java/C++ 标准库(如Arrays.binarySearch)的底层实现逻辑,面试 / 工作中写这个版本,面试官会认为你懂标准写法;
    • 唯一的 "反直觉"(right 初始为 nums.length),但记住 "右边界是数组长度" 即可,习惯后比全闭区间更高效。
  2. 新手过渡可选全闭区间 [left, right]

    • 先通过全闭区间理解二分逻辑,再过渡到左闭右开,避免一开始就被 "开区间" 绕晕。
  3. 左开右闭 / 全开区间:尽量不用

    • 实际应用场景极少,且容易因 "上中位数""循环条件" 写错导致死循环,面试中写这种写法,反而容易被面试官挑错。

十六、完整验证代码

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 任选一种lowerBound实现,结果一致
        int start = lowerBound_Closed(nums, target); 
        // int start = lowerBound(nums, target); // 左闭右开版
        // int start = lowerBound_LeftOpenRightClosed(nums, target); // 左开右闭版
        // int start = lowerBound_AllOpen(nums, target); // 全开版
        
        if (start == nums.length || nums[start] != target) {
            return new int[]{-1, -1};
        }
        int end = lowerBound_Closed(nums, target + 1) - 1;
        return new int[]{start, end};
    }

    // 左闭右开(推荐)
    private int lowerBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    // 全闭区间
    private int lowerBound_Closed(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int res = nums.length;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                res = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return res;
    }

    // 左开右闭区间
    private int lowerBound_LeftOpenRightClosed(int[] nums, int target) {
        int left = -1;
        int right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;
            if (nums[mid] >= target) {
                right = mid - 1;
            } else {
                left = mid;
            }
        }
        return left + 1;
    }

    // 全开区间
    private int lowerBound_AllOpen(int[] nums, int target) {
        int left = -1;
        int right = nums.length;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return right;
    }
}

相关推荐
点云SLAM2 小时前
C++ 中traits 类模板(type traits / customization traits)设计技术深度详解
c++·算法·c++模板·c++高级应用·traits 类模板·c++17、20·c++元信息
CoderYanger2 小时前
动态规划算法-两个数组的dp(含字符串数组):48.最长重复子数组
java·算法·leetcode·动态规划·1024程序员节
liu****3 小时前
9.二叉树(一)
c语言·开发语言·数据结构·算法·链表
sin_hielo3 小时前
leetcode 3577
数据结构·算法·leetcode
ACERT3333 小时前
04矩阵理论复习-矩阵的分解
算法·矩阵
csuzhucong3 小时前
快餐连锁大亨
算法
ssshooter3 小时前
小猫都能懂的大模型原理 1 - 深度学习基础
人工智能·算法·llm
慕容青峰4 小时前
【LeetCode 1925. 统计平方和三元组的数目 题解】
c++·算法·leetcode
冰西瓜6004 小时前
动态规划(一)算法设计与分析 国科大
算法·动态规划