概念
二分搜索概念
二分查找并不简单,Knuth 大佬(发明 KMP 算法的那位)都说二分查找:思路很简单,细节是魔鬼。很多人喜欢拿整型溢出的 bug 说事儿,但是二分查找真正的坑根本就不是那个细节问题,而是在于到底要给 mid 加一还是减一,while 里到底用 <= 还是 <。
你要是没有正确理解这些细节,写二分肯定就是玄学编程,有没有 bug 只能靠菩萨保佑(谁写谁知道)。我特意写了一首诗来歌颂该算法,概括本文的主要内容,建议保存(手动狗头):
摘自 labuladong
题目
统计一个数字在排序数组中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
提示:
0 <= nums.length <= 105-109 <= nums[i] <= 109nums是一个非递减数组-109 <= target <= 109
注意: 本题与主站 34 题相同(仅返回值不同):leetcode-cn.com/problems/fi…
解题思路🙋🏻 ♀️
- 首先,我们初始化
left为 0,right为nums.count - 1,即 5。我们还初始化first和end为 0。在进入第一次的 while 循环之前,变量的状态如下:
nums = [5,7,7,8,8,10]
l r
- 我们进入第一次的 while 循环,计算
middle = left + (right - left)/2 = 2。因为nums[middle] = 7 < 8,所以我们将left更新为middle + 1 = 3。循环继续,现在的状态如下:
nums = [5,7,7,8,8,10]
l r
M
- 继续第一次的 while 循环,计算
middle = left + (right - left)/2 = 3。因为nums[middle] = 8,所以我们将right更新为middle = 3。现在left == right,所以第一次的 while 循环结束,我们更新first = left = 3。现在的状态如下:
nums = [5,7,7,8,8,10]
l,
r
M
- 我们开始第二次的 while 循环,计算
middle = left + (right - left + 1)/2 = 4。因为nums[middle] = 8,所以我们将left更新为middle = 4。循环继续,现在的状态如下:
nums = [5,7,7,8,8,10]
l r
- 继续第二次的 while 循环,计算
middle = left + (right - left + 1)/2 = 4。因为nums[middle] = 8,所以我们将right更新为middle = 4。现在left == right,所以第二次的 while 循环结束,我们更新end = right = 4。现在的状态如下:
nums = [5,7,7,8,8,10]
l,
r
M
- 最后,我们返回
end - first + 1 = 2,这就是目标值 8 在数组中出现的次数。
边界思考🤔
// 如果数组中没有目标值,直接返回 0
if nums[left] != target {
return 0
}
代码
import Foundation
class Solution {
func search(_ nums: [Int], _ target: Int) -> Int {
// 如果数组为空,直接返回 0
if nums.count <= 0 {
return 0
}
// 初始化左右指针和首尾位置
var left = 0
var right = nums.count - 1
var first = 0
var end = 0
// 第一次二分查找,找到目标值的首次出现位置
while left < right {
let middle = left + (right - left)/2
if nums[middle] < target { // 如果中间值小于目标值,目标值在右侧,移动左指针
left = middle + 1
} else { // 如果中间值大于或等于目标值,目标值在左侧或就是中间值,移动右指针
right = middle
}
}
// 如果数组中没有目标值,直接返回 0
if nums[left] != target {
return 0
}
// 记录目标值的首次出现位置
first = left
// 重新设置右指针,进行第二次二分查找,找到目标值的末次出现位置
right = nums.count - 1
while left < right {
let middle = left + (right - left + 1)/2
if nums[middle] <= target { // 如果中间值小于或等于目标值,目标值在右侧或就是中间值,移动左指针
left = middle
} else { // 如果中间值大于目标值,目标值在左侧,移动右指针
right = middle - 1
}
}
// 记录目标值的末次出现位置
if nums[right] == target {
end = right
}
// 返回目标值的出现次数
return end - first + 1
}
}
时空复杂度分析
O (logn)