解法一:变种二分查找
每次把 nums[mid] 和数组最后一个数 nums[len-1]比较,
- 如果 nums[mid]>nums[n−1],那么可以推论:
- nums 一定被分成左右两个递增段;
- 第一段的所有元素均大于第二段的所有元素;
- nums[mid] 一定在第一段。
- 最小值在第二段。
- 所以最小值一定在 nums[mid]的右边,需要更新left,到右边继续找最小值
- 如果 nums[mid] <= nums[n−1],那么可以推论:
- nums[mid]一定在第二段(也可能数组没被旋转,那么就是全局递增数组,最小值就是第一个元素)
- nums[mid]要么是最小值,要么在最小值右边,需要更新right,到左边继续寻找最小值。
由于每次是和最后一个元素比较,因此二分搜索范围只需要在闭区间【0, len(nums)-2】,避免最小值在最后一个数时,循环退出条件left = right +1,导致数组越界的情况
func findMin(nums []int) int {
left, rignt := 0, len(nums)-2
for left <= rignt{
mid := left + (rignt - left)/2
if nums[mid] > nums[len(nums)-1] {
left = mid + 1
}else {
rignt = mid - 1
}
}
return nums[left]
}
换成开区间写法也可以
func findMin(nums []int) int {
left, right := 0, len(nums) - 1
for left < right {
pivot := left + (right - left) / 2
if nums[pivot] < nums[right] {
right = pivot
} else {
left = pivot + 1
}
}
return nums[left]
}
扩展:寻找旋转排序数组中的中位数
其实上面的解法,找到的最小值即为旋转排序数组的旋转点,把原数组分为左右两部分递增子数组,现在问题变成在两个有序递增数组中,找到中位数。
根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2个元素和第 (m+n)/2+1个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1。
// findPivot 用于找到旋转排序数组的旋转点
func findPivot(nums []int) int {
left, right := 0, len(nums)-1
for left < right {
mid := left + (right-left)/2
if nums[mid] > nums[right] {
left = mid + 1
} else {
right = mid
}
}
return left
}
// findMedian 用于找到旋转排序数组的中位数
func findMedian(nums []int) float64 {
n := len(nums)
pivot := findPivot(nums)
leftArr := nums[pivot:] // 数值较小的半边
rightArr := nums[:pivot] // 数值较大的半边
// 双指针法寻找中位数
i, j := 0, 0
var prev, current int
for k := 0; k <= n/2; k++ {
prev = current
if i < len(leftArr) && (j >= len(rightArr) || leftArr[i] < rightArr[j]) {
// `i < len(leftArr)` 确保 `i` 指针没有越界,即 `leftArr` 中还有元素未被遍历
// `j >= len(rightArr)` 表示 `rightArr` 中的元素已经全部遍历完
// `leftArr[i]` 是当前两个数组中较小的元素
current = leftArr[i]
i++
} else {
// `rightArr[j]` 是当前两个数组中较小的元素
current = rightArr[j]
j++
}
}
if n%2 == 1 {
return float64(current)
}
return float64(prev+current) / 2
}