【华为机试】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
解题思路
算法分析
这道题是二分查找和边界查找的经典应用。主要解法包括:
- 双重二分查找法:分别查找左边界和右边界
- 单次二分查找法:一次查找确定范围
- 线性查找法:暴力遍历(不满足时间复杂度要求)
- 库函数法:使用内置的查找函数
问题本质分析
双重二分查找详解
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[边界处理要准确]
各种解法对比
二分查找变种详解
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),通常只使用常数空间
关键优化点
实际应用场景
二分查找模板
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[边界查找模板]
算法扩展
测试用例设计
常见错误避免
代码实现要点
-
二分查找模板:
- 使用左闭右开区间避免边界错误
- mid计算使用
left + (right-left)/2
防溢出 - 明确left和right的更新规则
-
边界查找逻辑:
- 左边界:找第一个大于等于target的位置
- 右边界:找最后一个小于等于target的位置
- 验证找到的位置是否有效
-
特殊情况处理:
- 空数组直接返回[-1,-1]
- 目标值不存在返回[-1,-1]
- 单元素数组的边界情况
-
性能优化技巧:
- 提前判断边界情况
- 使用位运算优化除法
- 减少重复的边界检查
手工验证示例
这个问题的关键在于理解二分查找的边界处理和掌握左右边界的查找技巧,通过两次二分查找分别确定目标值的起始和结束位置。
完整题解代码
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("• 模板统一:使用一致的二分查找边界处理")
}