二分查找问题,想必对于开发人员来说都不陌生,但是想要准确而又熟练地写出二分查找及其变形问题的算法代码,对许多人来说,可能并不是一件很容易的事。
二分查找几种变形
二分查找,又称为⼆分搜索,一般有 以下几种变形题:
1. 查找第⼀次出现的等于给定目标值的元素(或元素下标)
2. 查找最后一次出现的等于给定目标值的元素(或元素下标)
3. 查找第⼀个⼤于等于给定值的元素(或元素下标)
4. 查找最后⼀个⼩于等于给定值的元素(或元素下标)
5. 查找给定目标值出现的起始和终止元素对应的位置(第1和第2种类型的结合)
对应的算法题及解题思路如下。
解题模板
既然是二分查找的变形,那么其解题套路离不开基本的二分查找的思想。基础的二分查找代码实现如下:
func binarySearch(nums []int, target int) int {
// low,high分别指向当前查找范围的首尾
low, high := 0, len(nums)-1
for low <= high { // 循环结束条件low > high
mid := low + (high - low) / 2
if nums[mid] > target { // 目标值在mid索引左侧,即在[low, mid-1]范围内
high = mid - 1
} else if nums[mid] < target { // 目标值在mid索引右侧,即在[mid+1, high]范围内
low = mid + 1
} else { // 函数结束,nums[mid] == target
return mid
}
}
return -1 // 不存在与目标值相等的元素,则返回-1
}
注意:这里二分查找循环结束的条件是low > high,这样处理方面与下面几种变形题保持一致,方便加深理解和记忆。
LeetCode35. 搜索插入位置
该题,如果使用遍历是很简单的,但是如果面试官要求时间复杂度为O(logn),可能就没那么容易毫不费时地写出来了。
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-insert-position
解题思路
对于⼀个从⼩到⼤排序后的数组,在数组中找到要插⼊的target 元素的位置,遍历即可以实现,那样的话时间复杂度是O(n),能不能使用O(logn)的复杂度实现呢?
答案是可以的,看到O(logn)可以联想到二分查找思想,而这⼀题就是一种⼆分查找的变种题,在有序数组中找到最后⼀个⽐ target ⼩的元素。
代码实现
注意:下面代码中,求中位数的方式使用low + (high - low) / 2的方式,而非(low + high) / 2,是因为当low和high都比较大时,两个大数相加可能超过int的范围,造成数值溢出;而使用low + (high - low) / 2的方式,对两个大数相减后再加另一个数字就可以很好地避免这种情况的发生。
// 查找数组中,最后一个比目标值target小的元素
func searchInsert(nums []int, target int) int {
low, high := 0, len(nums) - 1
for low <= high {
mid := low + (high - low) / 2
if nums[mid] < target { // 目标值在mid右侧,即[mid+1, high]范围内,
if mid == len(nums) - 1 || nums[mid+1] >= target { // 到达右边界,或者下一个元素大于目标值,则返回mid+1
return mid + 1
}
low = mid + 1
} else { // 目标值在mid左侧,即[low, mid]
high = mid - 1
}
}
return 0
}
LeetCode34. 在排序数组中查找元素的第一个和最后一个位置
题目描述
给定一个按照升序排列的整数数组 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]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array
解题思路
这⼀题是经典的⼆分搜索变种题。
解题思路可以分别利⽤ 上面提到的 变种1:“查找第⼀次出现的等于给定目标值的元素(或元素下标)” 和“查找最后⼀次出现的等于给定目标值的元素(或元素下标)”变种 2 的解法,将两者结合起来即可做出此题。或者⽤⼀次变种 1 的⽅法,然后循环往后找到最后⼀个与给定值相等的元素。不过后者这种⽅法可能会使时间复杂度下降到 O(n),因为有可能数组中 n 个元素都和给定元素相同。
代码实现
func searchRange(nums []int, target int) []int {
if len(nums) <= 0 {
return []int{-1, -1}
}
if findFirstTarget(nums, target) == -1 {
return []int{-1, -1}
}
return []int{findFirstTarget(nums, target), findLastTarget(nums, target)}
}
// 二分查找变形1:查找目标值第一次出现的位置
func findFirstTarget(nums []int, target int) int {
low, high := 0, len(nums) - 1
for low <= high {
mid := low + (high - low)/2
if nums[mid] > target { // 目标值在mid下标左侧,即在[low, mid-1]范围内
high = mid - 1
} else if nums[mid] < target { // 目标值在mid下标右侧,即在[mid+1, high]范围内
low = mid + 1
} else { // nums[mid] == target
if mid == 0 || nums[mid-1] != target { // 找到第一个与target相等的元素的索引,注意:判断mid是否是左边界
return mid
}
high = mid - 1
}
}
return -1
}
// 二分查找变形2:查找目标值最后一次出现的位置
func findLastTarget(nums []int, target int) int {
low, high := 0, len(nums) - 1
for low <= high {
mid := low + (high - low)/2
if nums[mid] > target {
high = mid - 1
} else if nums[mid] < target {
low = mid + 1
} else {
if mid == len(nums)-1 || nums[mid+1] != target { // 找到最后一个与target相等的元素的索引,注意:判断mid是否是右边界
return mid
}
low = mid + 1
}
}
return -1
}
33. 搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
解题思路
- 给出⼀个数组,数组中本来是从⼩到⼤排列的,并且数组中没有重复数字。但是现在把后⾯随机⼀段有序的放到数组前⾯,这样形成了前后两端有序的⼦序列。在这样的⼀个数组⾥⾯查找⼀个数,设计⼀个 O(log n) 的算法。如果找到就输出数组的⼩标,如果没有找到,就输出 -1 。
- 由于数组基本有序,虽然中间有⼀个“断开点”,还是可以使⽤⼆分搜索的算法来实现。现在数组前⾯⼀段是数值⽐较⼤的数,后⾯⼀段是数值偏⼩的数。如果 mid 落在了前⼀段数值⽐较⼤的区间内了,那么⼀定有 nums[mid] > nums[low] ,如果是落在后⾯⼀段数值⽐较⼩的区间内,nums[mid] ≤ nums[low] 。如果 mid 落在了后⼀段数值⽐较⼩的区间内了,那么⼀定有nums[mid] < nums[high] ,如果是落在前⾯⼀段数值⽐较⼤的区间内, nums[mid] ≤
nums[high] 。还有 nums[low] == nums[mid] 和 nums[high] == nums[mid] 的情况,单独处理即可。最后找到则输出 mid,没有找到则输出 -1 。
代码实现
func search(nums []int, target int) int {
return binarySearch(nums, target)
}
func binarySearch(nums []int, target int) int {
if len(nums) == 0 {
return -1
}
low, high := 0, len(nums)-1
for low <= high { // 循环终止条件low > high,不停更新low和high下标索引
mid := low + (high-low)/2
if nums[mid] == target { // 结束的终止条件
return mid
} else if nums[mid] > nums[low] { // 目标值在数值大的区间中
if nums[low] <= target && target < nums[mid] { // 注意:这里的<=的条件
high = mid - 1
} else {
low = mid + 1
}
} else if nums[mid] < nums[high] { // 目标值在数值小的区间中
if nums[mid] < target && target <= nums[high] { // 注意:这里的<=的条件
low = mid + 1
} else {
high = mid - 1
}
} else { // nums[high] <= nums[mid] <= nums[low]
if nums[mid] == nums[low] {
low++
}
if nums[mid] == nums[high] {
high--
}
}
}
return -1
}