我们接触到的二分查找,都是在一个有序数组找一个target,这个显然比较容易。但是如果我们换一种思路,在一个有序数组中找一个比target小的最大一个值,或者比target大的最小的一个值会怎么写呢?
给定一个数组nums = [1,2,3,4,6,7,19,22] target = 7。现在的条件是查找比target小且最大的一个值。在nums数组中6就是小于target且最大的一个值。那么大于target且最小的值就是19。
如果条件是大于等于target的一个值,这里需要考虑的点就是target可能不存在nums,要求你找到一个>=target的值。不存在返回nums.length就可以。
我们先回顾一下传统的二分查找在一个有序数组找等于target的val。
package main
func binarySearch(nums []int, target int) int {
//定义left,right
left , right := 0 , len(nums)
for left < right {
mid := (left + right) / 2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else {
return nums[mid]
}
}
return -1
}
1.1给定一个nums = [1,2,3,4,5,7,10] target = 6请找出大于等于target的第一个数.
package main
// binarySearch 在nums中找到第一个不target大的元素
func binarySearch(nums []int, target int) int {
left , right := 0, len(nums)
//这里是关键点,是否会死循环
//假设我们要的值是[9, 10]里面的10,L = 9, R = 10,(L + R) / 2 = mid,mid=9
//下一步mid + 1 最后left和right必然相等。
//如果不存在大于target的第一个元素就会返回数组最后一个元素。
for left < right {
mid := (left + right) / 2
//这里关键,判断是要往左还是往右。因为nums[mid] >= target 那就意味着 mid + 1 ~ right都不会成为答案。
if nums[mid] >= target {
right = mid
} else {
//假设小于的情况 left ~ mid都不会成为答案
left = mid + 1
}
}
return nums[right]
}
1.2给定一个nums = [1,2,3,4,5,7,10] target = 6请找出小于等于target的第一个数.
package main
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums) - 1
for left < right {
//给定一个数组[5,6] 我要的是6 L=5,R=6,(L+R+1)/2 这样方式就不会导致死循环。
//最终的结果让left=right才会退出循环。所以最后返回left和right都可以。
mid := (left + right + 1) / 2
if nums[mid] <= target {
left = mid
} else {
right = mid - 1
}
}
return nums[right]
}
总结:
简单来总结一下以上两种方式的区别。两个最终的结果都是在left=right才会退出循环。
找一个target的后继和前驱。
1.2前驱,当mid > target,那么mid + 1都不会是答案,剩下就在left ~ mid - 1这个区间。
1.1后继,当mid < target,那么left ~ mid都不会成为答案。剩下就在mid + 1 ~ right这个区间。
带着以上我们学习的二分搜索来看到一道题。leetcode 153. 寻找旋转排序数组中的最小值。
题目讲的是,给定一个排好序的旋转数组nums = [3,4,5,6,1,2]。不告诉你旋转了多少次。请你找出有序数组最小值。这个数组最小值就是1。
我先写一下朴素的做法。
package main
func findMin(nums []int) int {
ans := 1<<31 - 1
for i := range nums {
if ans > nums[i] {
ans = nums[i]
}
}
return ans
}
这样写出来的结果是O(n),假设要求时间复杂度是O(logn)
package main
func findMin(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 nums[right]
}
先来分析一下这个二分查找的假定给定一个数组nums = [3, 4, 5, 6, 7, 0, 1, 2]
如何寻找旋转数组中最小值呢!首先可以看看这个数组有什么特征,每一段都满足单调性。(L + R) / 2取mid,那就是中间这个位置,假设中间这个位置nums[mid] > nums[right] 说明最小的一段在mid + 1 ~ right这里。left = mid + 1,某一个时刻(L + R) / 2 = mid < right的情况,让right = mid,因为mid有可能是我们要的答案。当数组内只有两个元素[0,1] 0 -> Left, 1 -> Right,当mid 取到0 < right ,我们让right = mid,此时退出循环。返回left或right都可以。
退出循环最终是停留在left=right。