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

问题简介

LeetCode 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]

解题思路

💡 核心思想: 由于数组已排序,我们可以使用二分查找 来达到 O(log n) 的时间复杂度。但标准二分查找只能找到任意一个目标值的位置,我们需要分别找到第一个位置最后一个位置

方法一:两次二分查找(推荐)

📌 步骤分解:

  1. 查找第一个位置(左边界):

    • nums[mid] == target 时,不立即返回,而是继续在左半部分查找
    • 记录可能的左边界位置
  2. 查找最后一个位置(右边界):

    • nums[mid] == target 时,不立即返回,而是继续在右半部分查找
    • 记录可能的右边界位置
  3. 验证结果:

    • 如果左边界有效且对应值等于 target,则返回 [left, right]
    • 否则返回 [-1, -1]

方法二:一次二分查找 + 线性扩展(不推荐)

为什么不推荐:

  • 虽然平均情况可能不错,但最坏情况下(整个数组都是 target)时间复杂度为 O(n)
  • 不满足题目要求的 O(log n) 时间复杂度

方法三:使用内置函数(语言相关)

💡 思路: 某些语言提供查找第一个/最后一个位置的内置函数

  • Java: Arrays.binarySearch() 需要额外处理
  • Go: 需要手动实现或使用第三方库

结论:方法一是最优解!


代码实现

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int left = findFirst(nums, target);
        int right = findLast(nums, target);
        
        // 验证结果是否有效
        if (left <= right && left >= 0 && right < nums.length && 
            nums[left] == target && nums[right] == target) {
            return new int[]{left, right};
        }
        return new int[]{-1, -1};
    }
    
    // 查找第一个位置
    private int findFirst(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        int result = -1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                result = mid;        // 记录可能的位置
                right = mid - 1;     // 继续在左半部分查找
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }
    
    // 查找最后一个位置
    private int findLast(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        int result = -1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                result = mid;        // 记录可能的位置
                left = mid + 1;      // 继续在右半部分查找
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }
}
go 复制代码
func searchRange(nums []int, target int) []int {
    left := findFirst(nums, target)
    right := findLast(nums, target)
    
    // 验证结果是否有效
    if left <= right && left >= 0 && right < len(nums) && 
       nums[left] == target && nums[right] == target {
        return []int{left, right}
    }
    return []int{-1, -1}
}

// 查找第一个位置
func findFirst(nums []int, target int) int {
    left, right := 0, len(nums)-1
    result := -1
    
    for left <= right {
        mid := left + (right-left)/2
        if nums[mid] == target {
            result = mid        // 记录可能的位置
            right = mid - 1     // 继续在左半部分查找
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return result
}

// 查找最后一个位置
func findLast(nums []int, target int) int {
    left, right := 0, len(nums)-1
    result := -1
    
    for left <= right {
        mid := left + (right-left)/2
        if nums[mid] == target {
            result = mid        // 记录可能的位置
            left = mid + 1      // 继续在右半部分查找
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return result
}

示例演示

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

查找第一个位置(findFirst)

步骤 left right mid nums[mid] 操作
1 0 5 2 7 left = 3
2 3 5 4 8 result = 4, right = 3
3 3 3 3 8 result = 3, right = 2
结束 3 > 2 - - - 返回 3

查找最后一个位置(findLast)

步骤 left right mid nums[mid] 操作
1 0 5 2 7 left = 3
2 3 5 4 8 result = 4, left = 5
3 5 5 5 10 right = 4
结束 5 > 4 - - - 返回 4

✅ 最终结果:[3, 4]


答案有效性证明

✅ 正确性证明

  1. 左边界查找正确性:

    • 当找到 target 时,继续向左搜索确保找到的是第一个位置
    • 循环不变式:result 始终记录已找到的最左边的 target 位置
  2. 右边界查找正确性:

    • 当找到 target 时,继续向右搜索确保找到的是最后一个位置
    • 循环不变式:result 始终记录已找到的最右边的 target 位置
  3. 边界情况处理:

    • 空数组:直接返回 [-1, -1]
    • 不存在 target:两个查找都返回 -1
    • 单个元素:左右边界相同

✅ 循环终止性

  • 每次迭代要么 left 增加,要么 right 减少
  • 区间 [left, right] 严格缩小,最终必然终止

复杂度分析

分析维度 复杂度 说明
时间复杂度 O(log n) 两次二分查找,每次 O(log n)
空间复杂度 O(1) 只使用常数额外空间

📊 详细分析:

  • 时间: 每次二分查找最多执行 log₂(n) 次比较,两次查找总共 2 × log₂(n) = O(log n)
  • 空间: 仅使用几个变量存储索引和结果,无递归调用栈

问题总结

📌 关键要点:

  1. 二分查找变种: 标准二分查找找到任意位置,而本题需要找到边界位置
  2. 边界处理技巧: 找到目标值后不立即返回,而是继续在相应方向搜索
  3. 结果验证: 必须验证找到的边界是否有效(防止空数组或不存在的情况)

💡 通用模板:

java 复制代码
// 查找左边界
while (left <= right) {
    if (nums[mid] == target) {
        result = mid;
        right = mid - 1;  // 关键:继续向左
    }
    // ... 其他逻辑
}

// 查找右边界  
while (left <= right) {
    if (nums[mid] == target) {
        result = mid;
        left = mid + 1;   // 关键:继续向右
    }
    // ... 其他逻辑
}

适用场景:

  • 在有序数组中查找元素的边界位置
  • 需要 O(log n) 时间复杂度的搜索问题
  • 类似问题:查找插入位置、查找大于/小于某值的元素等

github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

相关推荐
爱凤的小光2 小时前
VisionMaster软件---脚本梳理
java·服务器·网络
我命由我123452 小时前
Photoshop - Photoshop 工具栏(63)注释工具
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
键盘鼓手苏苏4 小时前
Flutter for OpenHarmony:markdown 纯 Dart 解析引擎(将文本转化为结构化 HTML/UI) 深度解析与鸿蒙适配指南
前端·网络·算法·flutter·ui·html·harmonyos
郝学胜-神的一滴5 小时前
当AI遇见架构:Vibe Coding时代的设计模式复兴
开发语言·数据结构·人工智能·算法·设计模式·架构
Frostnova丶10 小时前
LeetCode 190.颠倒二进制位
java·算法·leetcode
骇城迷影10 小时前
代码随想录:链表篇
数据结构·算法·链表
闻哥11 小时前
Redis事务详解
java·数据库·spring boot·redis·缓存·面试
hrhcode11 小时前
【Netty】五.ByteBuf内存管理深度剖析
java·后端·spring·springboot·netty
道亦无名11 小时前
aiPbMgrSendAck
java·网络·数据库