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

文章目录

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 TD A[输入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 TD A[左边界查找] --> 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 --> C H --> C D --> I[检查边界有效性]

右边界查找过程

flowchart TD A[右边界查找] --> 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 --> C H --> C D --> I[检查边界有效性]

算法流程图

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

边界情况分析

graph TD A[边界情况] --> 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 TD A[二分查找变种] --> 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 TD A[二分查找模板选择] --> 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 --> I H --> 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]

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

完整题解代码

go 复制代码
package main

import (
	"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)/2
		if 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)/2
		if 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)-1

	for left <= right {
		mid := left + (right-left)/2
		if 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)-1

	for left <= right {
		mid := left + (right-left)/2
		if 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)-1

	for left <= right {
		mid := left + (right-left)/2
		if 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的位置减1
	rightBound := sort.SearchInts(nums, target+1) - 1

	return []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 = i
			break
		}
	}

	if left == -1 {
		return []int{-1, -1}
	}

	// 从右到左找最后一个目标值
	for i := len(nums) - 1; i >= 0; i-- {
		if nums[i] == target {
			right = i
			break
		}
	}

	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)/2

	if 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)/2

	if 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     []int
		target   int
		expected []int
		desc     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   []int
		target int
		desc   string
	}{
		{generateSortedArray(1000, 5), 5, "1000元素数组"},
		{generateSortedArray(10000, 50), 50, "10000元素数组"},
		{generateSortedArray(100000, 500), 500, "100000元素数组"},
		{generateRepeatedArray(50000, 42), 42, "50000重复元素"},
	}

	algorithms := []struct {
		name string
		fn   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 / 2

	for 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 := 8

	fmt.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 := 1

	fmt.Printf("初始: left=%d, right=%d\n", left, right)

	for left < right {
		mid := left + (right-left)/2
		fmt.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 + 1
			fmt.Printf("       nums[%d]=%d < %d, left=%d\n", mid, nums[mid], target, left)
		} else {
			right = mid
			fmt.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 := 1

	fmt.Printf("初始: left=%d, right=%d\n", left, right)

	for left < right {
		mid := left + (right-left)/2
		fmt.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 + 1
			fmt.Printf("       nums[%d]=%d <= %d, left=%d\n", mid, nums[mid], target, left)
		} else {
			right = mid
			fmt.Printf("       nums[%d]=%d > %d, right=%d\n", mid, nums[mid], target, right)
		}
		step++
	}

	rightBound := left - 1
	fmt.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("• 模板统一:使用一致的二分查找边界处理")
}
相关推荐
程序员Xu1 小时前
【OD机试题解法笔记】查找接口成功率最优时间段
笔记·算法
云泽8082 小时前
数据结构前篇 - 深入解析数据结构之复杂度
c语言·开发语言·数据结构
逝雪Yuki2 小时前
数据结构与算法——字典(前缀)树的实现
数据结构·c++·字典树·前缀树·左程云
技术思考者2 小时前
Leetcode - 反转字符串
数据结构·算法·leetcode
SKYDROID云卓小助手3 小时前
无人设备遥控器之多设备协同技术篇
网络·人工智能·嵌入式硬件·算法·信号处理
phltxy3 小时前
ArrayList与顺序表
java·算法
草莓熊Lotso4 小时前
【数据结构初阶】--排序(一):直接插入排序,希尔排序
c语言·数据结构·经验分享·其他·排序算法
小拇指~5 小时前
梯度下降的基本原理
人工智能·算法·计算机视觉