当前位置: 首页 > news >正文

【华为机试】34. 在排序数组中查找元素的第一个和最后一个位置

文章目录

  • 34. 在排序数组中查找元素的第一个和最后一个位置
    • 描述
    • 示例 1:
    • 示例 2:
    • 示例 3:
    • 提示:
    • 解题思路
      • 算法分析
      • 问题本质分析
      • 双重二分查找详解
      • 左边界查找过程
      • 右边界查找过程
      • 算法流程图
      • 边界情况分析
      • 各种解法对比
      • 二分查找变种详解
      • 时间复杂度分析
      • 空间复杂度分析
      • 关键优化点
      • 实际应用场景
      • 二分查找模板
      • 算法扩展
      • 测试用例设计
      • 常见错误避免
      • 代码实现要点
      • 手工验证示例
    • 完整题解代码

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]

提示:

  • 0 <= nums.length <= 105
  • -10^9 <= nums[i] <= 10^9
  • nums 是一个非递减数组
  • -10^9 <= target <= 10^9

解题思路

算法分析

这道题是二分查找边界查找的经典应用。主要解法包括:

  1. 双重二分查找法:分别查找左边界和右边界
  2. 单次二分查找法:一次查找确定范围
  3. 线性查找法:暴力遍历(不满足时间复杂度要求)
  4. 库函数法:使用内置的查找函数

问题本质分析

排序数组范围查找
二分查找变种
左边界查找
右边界查找
范围确定
找到第一个>=target的位置
找到最后一个<=target的位置
合并两个边界结果
时间复杂度O_logn

双重二分查找详解

flowchart TDA[输入nums和target] --> B[查找左边界]B --> C[left_bound = findLeft]C --> D{左边界是否存在}D -->|不存在| E[返回[-1,-1]]D -->|存在| F[查找右边界]F --> G[right_bound = findRight]G --> H[返回[left_bound, right_bound]]B --> I[左边界二分查找]I --> J[寻找第一个>=target的位置]F --> K[右边界二分查找]K --> L[寻找最后一个<=target的位置]

左边界查找过程

flowchart TDA[左边界查找] --> B[初始化left=0, right=len]B --> C{left < right}C -->|否| D[返回left]C -->|是| E[计算mid = left + (right-left)/2]E --> F{nums[mid] < target}F -->|是| G[left = mid + 1]F -->|否| H[right = mid]G --> CH --> CD --> I[检查边界有效性]

右边界查找过程

flowchart TDA[右边界查找] --> B[初始化left=0, right=len]B --> C{left < right}C -->|否| D[返回left-1]C -->|是| E[计算mid = left + (right-left)/2]E --> F{nums[mid] <= target}F -->|是| G[left = mid + 1]F -->|否| H[right = mid]G --> CH --> CD --> I[检查边界有效性]

算法流程图

flowchart TDA[开始] --> B[输入验证]B --> C{数组为空}C -->|是| D[返回[-1,-1]]C -->|否| E[查找左边界]E --> F[二分查找第一个>=target]F --> G{找到有效位置}G -->|否| DG -->|是| H{nums[left]==target}H -->|否| DH -->|是| I[查找右边界]I --> J[二分查找最后一个<=target]J --> K[返回[left, right]]

边界情况分析

graph TDA[边界情况] --> B[空数组]A --> C[单元素数组]A --> D[目标不存在]A --> E[全部相同元素]A --> F[目标在首尾]B --> G[直接返回[-1,-1]]C --> H[判断唯一元素是否匹配]D --> I[两次二分都找不到]E --> J[返回[0, n-1]]F --> K[边界处理要准确]

各种解法对比

解法对比
双重二分查找
单次二分查找
线性查找
库函数法
时间O_logn空间O_1
时间O_logn空间O_1
时间O_n空间O_1
时间O_logn空间O_1
推荐解法清晰易懂
代码紧凑但复杂
不满足题目要求
语言特定实现

二分查找变种详解

graph TDA[二分查找变种] --> B[查找确切值]A --> C[查找左边界]A --> D[查找右边界]A --> E[查找插入位置]B --> F[nums[mid] == target时直接返回]C --> G[nums[mid] >= target时收缩右边界]D --> H[nums[mid] <= target时收缩左边界]E --> I[找到第一个>target的位置]F --> J[经典二分查找]G --> K[左边界二分]H --> L[右边界二分]I --> M[插入位置二分]

时间复杂度分析

  • 双重二分查找:O(log n),执行两次独立的二分查找
  • 单次二分查找:O(log n),一次遍历确定范围
  • 线性查找:O(n),不满足题目要求
  • 库函数查找:O(log n),依赖具体实现

空间复杂度分析

  • 双重二分查找:O(1),只使用常数额外空间
  • 单次二分查找:O(1),只使用常数额外空间
  • 线性查找:O(1),只使用常数额外空间
  • 库函数查找:O(1),通常只使用常数空间

关键优化点

优化策略
边界处理
提前退出
溢出防护
代码复用
准确定义开闭区间
空数组直接返回
mid计算防止溢出
统一二分查找模板
避免边界错误

实际应用场景

应用场景
数据库索引
搜索引擎
数据分析
算法竞赛
范围查询优化
相关性排序查找
时间序列数据范围
区间查找问题
核心算法组件

二分查找模板

flowchart TDA[二分查找模板选择] --> B[左闭右闭 [left, right]]A --> C[左闭右开 [left, right)]A --> D[左开右开 (left, right)]B --> E[right = len - 1]C --> F[right = len]D --> G[left = -1, right = len]E --> H[while left <= right]F --> I[while left < right]G --> IH --> J[经典二分模板]I --> K[边界查找模板]

算法扩展

算法扩展
查找峰值
旋转数组查找
二维矩阵查找
第K小元素
山峰数组问题
分治策略
行列有序矩阵
快速选择算法
二分查找家族

测试用例设计

测试用例
基础功能
边界情况
性能测试
目标存在
目标不存在
重复元素
空数组
单元素
全相同
大数组
最坏情况
验证正确性
验证性能

常见错误避免

常见错误
边界计算错误
无限循环
整数溢出
边界条件遗漏
左右边界定义不一致
left和right更新错误
mid计算溢出
特殊情况未处理
使用统一模板
保证循环收敛
使用安全计算
完善测试用例

代码实现要点

  1. 二分查找模板

    • 使用左闭右开区间避免边界错误
    • mid计算使用left + (right-left)/2防溢出
    • 明确left和right的更新规则
  2. 边界查找逻辑

    • 左边界:找第一个大于等于target的位置
    • 右边界:找最后一个小于等于target的位置
    • 验证找到的位置是否有效
  3. 特殊情况处理

    • 空数组直接返回[-1,-1]
    • 目标值不存在返回[-1,-1]
    • 单元素数组的边界情况
  4. 性能优化技巧

    • 提前判断边界情况
    • 使用位运算优化除法
    • 减少重复的边界检查

手工验证示例

数组[5,7,7,8,8,10], target=8
查找左边界
left=0, right=6
mid=3, nums[3]=8 >= 8, right=3
mid=1, nums[1]=7 < 8, left=2
mid=2, nums[2]=7 < 8, left=3
left=right=3, 左边界=3
查找右边界
left=0, right=6
mid=3, nums[3]=8 <= 8, left=4
mid=5, nums[5]=10 > 8, right=5
mid=4, nums[4]=8 <= 8, left=5
left=right=5, 右边界=4
返回[3,4]

这个问题的关键在于理解二分查找的边界处理掌握左右边界的查找技巧,通过两次二分查找分别确定目标值的起始和结束位置。

完整题解代码

package mainimport ("fmt""sort""strings""time"
)// 解法一:双重二分查找法(推荐解法)
// 时间复杂度:O(log n),空间复杂度:O(1)
func searchRange(nums []int, target int) []int {if len(nums) == 0 {return []int{-1, -1}}// 查找左边界leftBound := findLeftBound(nums, target)if leftBound == -1 {return []int{-1, -1}}// 查找右边界rightBound := findRightBound(nums, target)return []int{leftBound, rightBound}
}// 查找左边界:第一个大于等于target的位置
func findLeftBound(nums []int, target int) int {left, right := 0, len(nums)for left < right {mid := left + (right-left)/2if nums[mid] < target {left = mid + 1} else {right = mid}}// 检查找到的位置是否有效if left < len(nums) && nums[left] == target {return left}return -1
}// 查找右边界:最后一个小于等于target的位置
func findRightBound(nums []int, target int) int {left, right := 0, len(nums)for left < right {mid := left + (right-left)/2if nums[mid] <= target {left = mid + 1} else {right = mid}}// left-1是最后一个小于等于target的位置return left - 1
}// 解法二:优化的双重二分查找(更清晰的边界处理)
// 时间复杂度:O(log n),空间复杂度:O(1)
func searchRangeOptimized(nums []int, target int) []int {if len(nums) == 0 {return []int{-1, -1}}// 使用统一的二分查找模板leftBound := binarySearchLeft(nums, target)rightBound := binarySearchRight(nums, target)if leftBound <= rightBound {return []int{leftBound, rightBound}}return []int{-1, -1}
}// 二分查找左边界(左闭右闭区间)
func binarySearchLeft(nums []int, target int) int {left, right := 0, len(nums)-1for left <= right {mid := left + (right-left)/2if nums[mid] >= target {right = mid - 1} else {left = mid + 1}}// 检查边界和目标值if left < len(nums) && nums[left] == target {return left}return len(nums) // 表示未找到
}// 二分查找右边界(左闭右闭区间)
func binarySearchRight(nums []int, target int) int {left, right := 0, len(nums)-1for left <= right {mid := left + (right-left)/2if nums[mid] <= target {left = mid + 1} else {right = mid - 1}}// 检查边界和目标值if right >= 0 && nums[right] == target {return right}return -1 // 表示未找到
}// 解法三:单次二分查找法
// 时间复杂度:O(log n),空间复杂度:O(1)
func searchRangeSingle(nums []int, target int) []int {if len(nums) == 0 {return []int{-1, -1}}// 先用标准二分查找找到任意一个target位置pos := binarySearch(nums, target)if pos == -1 {return []int{-1, -1}}// 从找到的位置向两边扩展left, right := pos, pos// 向左扩展找到左边界for left > 0 && nums[left-1] == target {left--}// 向右扩展找到右边界for right < len(nums)-1 && nums[right+1] == target {right++}return []int{left, right}
}// 标准二分查找
func binarySearch(nums []int, target int) int {left, right := 0, len(nums)-1for left <= right {mid := left + (right-left)/2if nums[mid] == target {return mid} else if nums[mid] < target {left = mid + 1} else {right = mid - 1}}return -1
}// 解法四:使用Go标准库
// 时间复杂度:O(log n),空间复杂度:O(1)
func searchRangeStdLib(nums []int, target int) []int {if len(nums) == 0 {return []int{-1, -1}}// 使用sort.SearchInts查找左边界leftBound := sort.SearchInts(nums, target)// 检查是否找到目标值if leftBound >= len(nums) || nums[leftBound] != target {return []int{-1, -1}}// 查找右边界:第一个大于target的位置减1rightBound := sort.SearchInts(nums, target+1) - 1return []int{leftBound, rightBound}
}// 解法五:线性查找法(不满足时间复杂度要求,仅用于对比)
// 时间复杂度:O(n),空间复杂度:O(1)
func searchRangeLinear(nums []int, target int) []int {left, right := -1, -1// 从左到右找第一个目标值for i := 0; i < len(nums); i++ {if nums[i] == target {left = ibreak}}if left == -1 {return []int{-1, -1}}// 从右到左找最后一个目标值for i := len(nums) - 1; i >= 0; i-- {if nums[i] == target {right = ibreak}}return []int{left, right}
}// 解法六:递归二分查找
// 时间复杂度:O(log n),空间复杂度:O(log n)
func searchRangeRecursive(nums []int, target int) []int {if len(nums) == 0 {return []int{-1, -1}}left := findLeftBoundRecursive(nums, target, 0, len(nums)-1)if left == -1 {return []int{-1, -1}}right := findRightBoundRecursive(nums, target, 0, len(nums)-1)return []int{left, right}
}// 递归查找左边界
func findLeftBoundRecursive(nums []int, target, left, right int) int {if left > right {return -1}mid := left + (right-left)/2if nums[mid] == target {// 检查是否是最左边的if mid == 0 || nums[mid-1] != target {return mid}return findLeftBoundRecursive(nums, target, left, mid-1)} else if nums[mid] < target {return findLeftBoundRecursive(nums, target, mid+1, right)} else {return findLeftBoundRecursive(nums, target, left, mid-1)}
}// 递归查找右边界
func findRightBoundRecursive(nums []int, target, left, right int) int {if left > right {return -1}mid := left + (right-left)/2if nums[mid] == target {// 检查是否是最右边的if mid == len(nums)-1 || nums[mid+1] != target {return mid}return findRightBoundRecursive(nums, target, mid+1, right)} else if nums[mid] < target {return findRightBoundRecursive(nums, target, mid+1, right)} else {return findRightBoundRecursive(nums, target, left, mid-1)}
}// 测试函数
func testSearchRange() {testCases := []struct {nums     []inttarget   intexpected []intdesc     string}{{[]int{5, 7, 7, 8, 8, 10}, 8, []int{3, 4}, "示例1:目标存在多个"},{[]int{5, 7, 7, 8, 8, 10}, 6, []int{-1, -1}, "示例2:目标不存在"},{[]int{}, 0, []int{-1, -1}, "示例3:空数组"},{[]int{1}, 1, []int{0, 0}, "单元素匹配"},{[]int{1}, 2, []int{-1, -1}, "单元素不匹配"},{[]int{1, 1, 1, 1, 1}, 1, []int{0, 4}, "全部相同元素"},{[]int{1, 2, 3, 4, 5}, 1, []int{0, 0}, "目标在首位"},{[]int{1, 2, 3, 4, 5}, 5, []int{4, 4}, "目标在末位"},{[]int{1, 2, 3, 4, 5}, 3, []int{2, 2}, "目标在中间单个"},{[]int{1, 2, 2, 2, 3}, 2, []int{1, 3}, "目标在中间多个"},{[]int{1, 3, 5, 7, 9}, 4, []int{-1, -1}, "目标在间隙"},{[]int{1, 1, 2, 2, 3, 3}, 2, []int{2, 3}, "连续重复"},{[]int{-1, 0, 3, 5, 9, 12}, 9, []int{4, 4}, "包含负数"},{[]int{-3, -1, 0, 3, 5}, -1, []int{1, 1}, "负数目标"},{[]int{0, 0, 0, 1, 1, 1}, 0, []int{0, 2}, "零值连续"},}fmt.Println("=== 查找元素范围测试 ===\n")for i, tc := range testCases {// 测试主要解法result1 := searchRange(tc.nums, tc.target)result2 := searchRangeOptimized(tc.nums, tc.target)result3 := searchRangeStdLib(tc.nums, tc.target)status := "✅"if !equalSlices(result1, tc.expected) {status = "❌"}fmt.Printf("测试 %d: %s\n", i+1, tc.desc)fmt.Printf("输入: nums=%v, target=%d\n", tc.nums, tc.target)fmt.Printf("期望: %v\n", tc.expected)fmt.Printf("双重二分: %v\n", result1)fmt.Printf("优化二分: %v\n", result2)fmt.Printf("标准库法: %v\n", result3)fmt.Printf("结果: %s\n", status)fmt.Println(strings.Repeat("-", 40))}
}// 辅助函数:比较两个切片是否相等
func equalSlices(a, b []int) bool {if len(a) != len(b) {return false}for i := range a {if a[i] != b[i] {return false}}return true
}// 性能测试
func benchmarkSearchRange() {fmt.Println("\n=== 性能测试 ===\n")// 构造测试数据testData := []struct {nums   []inttarget intdesc   string}{{generateSortedArray(1000, 5), 5, "1000元素数组"},{generateSortedArray(10000, 50), 50, "10000元素数组"},{generateSortedArray(100000, 500), 500, "100000元素数组"},{generateRepeatedArray(50000, 42), 42, "50000重复元素"},}algorithms := []struct {name stringfn   func([]int, int) []int}{{"双重二分", searchRange},{"优化二分", searchRangeOptimized},{"标准库法", searchRangeStdLib},{"递归二分", searchRangeRecursive},{"线性查找", searchRangeLinear},}for _, data := range testData {fmt.Printf("%s:\n", data.desc)for _, algo := range algorithms {start := time.Now()result := algo.fn(data.nums, data.target)duration := time.Since(start)fmt.Printf("  %s: %v, 耗时: %v\n", algo.name, result, duration)}fmt.Println()}
}// 生成有序数组(包含重复元素)
func generateSortedArray(size, targetCount int) []int {nums := make([]int, size)target := size / 2for i := 0; i < size; i++ {if i >= target && i < target+targetCount {nums[i] = target} else if i < target {nums[i] = i} else {nums[i] = i - targetCount + 1}}return nums
}// 生成重复元素数组
func generateRepeatedArray(size, value int) []int {nums := make([]int, size)for i := range nums {if i < size/3 {nums[i] = value - 1} else if i < 2*size/3 {nums[i] = value} else {nums[i] = value + 1}}return nums
}// 演示二分查找过程
func demonstrateBinarySearch() {fmt.Println("\n=== 二分查找过程演示 ===")nums := []int{5, 7, 7, 8, 8, 10}target := 8fmt.Printf("数组: %v, 目标: %d\n", nums, target)fmt.Println("\n查找左边界过程:")demonstrateLeftBound(nums, target)fmt.Println("\n查找右边界过程:")demonstrateRightBound(nums, target)result := searchRange(nums, target)fmt.Printf("\n最终结果: %v\n", result)
}func demonstrateLeftBound(nums []int, target int) {left, right := 0, len(nums)step := 1fmt.Printf("初始: left=%d, right=%d\n", left, right)for left < right {mid := left + (right-left)/2fmt.Printf("步骤%d: left=%d, right=%d, mid=%d, nums[%d]=%d\n",step, left, right, mid, mid, nums[mid])if nums[mid] < target {left = mid + 1fmt.Printf("       nums[%d]=%d < %d, left=%d\n", mid, nums[mid], target, left)} else {right = midfmt.Printf("       nums[%d]=%d >= %d, right=%d\n", mid, nums[mid], target, right)}step++}if left < len(nums) && nums[left] == target {fmt.Printf("找到左边界: %d\n", left)} else {fmt.Println("未找到目标值")}
}func demonstrateRightBound(nums []int, target int) {left, right := 0, len(nums)step := 1fmt.Printf("初始: left=%d, right=%d\n", left, right)for left < right {mid := left + (right-left)/2fmt.Printf("步骤%d: left=%d, right=%d, mid=%d, nums[%d]=%d\n",step, left, right, mid, mid, nums[mid])if nums[mid] <= target {left = mid + 1fmt.Printf("       nums[%d]=%d <= %d, left=%d\n", mid, nums[mid], target, left)} else {right = midfmt.Printf("       nums[%d]=%d > %d, right=%d\n", mid, nums[mid], target, right)}step++}rightBound := left - 1fmt.Printf("找到右边界: %d\n", rightBound)
}func main() {fmt.Println("34. 在排序数组中查找元素的第一个和最后一个位置")fmt.Println("================================================")// 基础功能测试testSearchRange()// 性能对比测试benchmarkSearchRange()// 二分查找过程演示demonstrateBinarySearch()// 展示算法特点fmt.Println("\n=== 算法特点分析 ===")fmt.Println("1. 双重二分:经典解法,两次独立二分查找,清晰易懂")fmt.Println("2. 优化二分:统一模板,边界处理更加清晰")fmt.Println("3. 标准库法:利用内置函数,代码简洁")fmt.Println("4. 递归二分:递归实现,代码简洁但有栈溢出风险")fmt.Println("5. 线性查找:时间复杂度O(n),不满足题目要求")fmt.Println("\n=== 关键技巧总结 ===")fmt.Println("• 左边界查找:找第一个>=target的位置")fmt.Println("• 右边界查找:找最后一个<=target的位置")fmt.Println("• 边界检查:确保找到的位置值等于target")fmt.Println("• 溢出防护:mid计算使用left+(right-left)/2")fmt.Println("• 模板统一:使用一致的二分查找边界处理")
}
http://www.dtcms.com/a/310616.html

相关文章:

  • 移动端 WebView 内存泄漏与性能退化问题如何排查 实战调试方法汇总
  • 文章发布Typecho网站技巧
  • Squid服务配置代理
  • SystemVerilog的系统函数和任务
  • Python 项目路径配置完全指南
  • C语言-字符串(定义)、字符串函数(strlen、strcat、strcpy、strcmp、strlwr、strupr)
  • 航天器VHF/UHF/L频段弱电磁信号兼容性设计
  • 【3】交互式图表制作及应用方法
  • Spring Cloud 和服务拆分:微服务落地的第一步
  • Java抽象类与接口深度解析:核心区别与应用场景全指南
  • C++ - 仿 RabbitMQ 实现消息队列--服务端核心模块实现(五)
  • 流式输出:概念、技巧与常见问题
  • c++详解(宏与内联函数,nullptr)
  • 每日面试题18:基本数据类型和引用数据类型的区别
  • 唐克的新游戏
  • 100道题通过CISSP,还剩70分钟
  • 体育数据API接入方式与数据类型详解
  • 连载【流程规划进阶 16/16】完结——35页16.流程的现状分析 【附全文阅读】
  • 达梦数据库权限体系详解:系统权限与对象权限
  • 大模型微调与部署课程笔记
  • FreeRTOS硬件中断发生时的现场
  • Spring AI 与 LangChain4j 对比及入门案例解析
  • Selenium:强大的 Web 自动化测试工具
  • VS Code中配置使用slint(Rust)的一个小例子
  • 亚马逊广告:如何借助AI玩转长尾词提升ROI
  • 伞状Meta分析重构癌症幸存者照护指南:从矛盾证据到精准决策
  • (28)运动目标检测之随机曲线上的离散点进行插值
  • 金智维董事长廖万里出席2025中国科创投资夏季峰会,共话智能体垂直落地新路径
  • deepseek: 批量处理脚本
  • shell脚本的语法使用及例题