二分查找
理论部分 (二分查找的优化)
二分查找最重要的地方在于两点:
- 确定查找区间
- if条件
区间可以有很多种, 左闭右闭, 左闭右开, 左开右闭, 左开右开怎么写都可以
通常我们使用 左闭右闭和左闭右开区间, 也就是 [left, right] 和[left, right)
区间可以帮我们确定 while/for 循环的条件
例如左闭右闭的话, 代表left可以等于right, 就得这么写: while(left <=right)
左闭右开的话, 代表 left 和 right 相等时是没有意义的, 需要 while(left < right)
我推荐使用左闭右开区间, 原因在后面
if条件的话查找区间不同, 左右指针变化情况也不同
如果是左闭右闭区间, 左右指针变化情况为:
if nums[mid] > target{
right = mid - 1
}else if nums[mid] < target{
left = mid + 1
}else{
return mid
}
如果是左闭右开的区间, if的分支为:
if nums[mid] > target{
right = mid
}else if nums[mid] < target{
left = mid + 1
}else{
return mid
}
if 的写法其实可以改进, 如果我们查看python源码中关于二分查找的代码,可以看到是这么写的:
def bisect_right(a, x, lo=0, hi=None):
if lo < 0:
raise ValueError('lo must be non-negative')
if hi is None:
hi = len(a)
while lo < hi:
mid = (lo+hi)//2
if x < a[mid]:
hi = mid
else:
lo = mid+1
return lo
他将if分支分为了 类似nums[mid] < target 和 nums[mid] >= target这样写法的两个分支, 而go语言源码的二分查找也是类似的写法, 并且两个语言都用的左闭右开区间
这样做有什么好处呢?
- 当有多个元素都等于target时,实际上可以找到下标最小的那个元素
- 当target不存在时,返回的下标标识了如果要将target插入,下标表示的就是插入的位置(插入后依然保持数组有序)
也就是说,这样写的话, 如果存在target, 最后left总会指向数组中数值等于target的第一个位置, 如果不存在target, left总会指向第一个比target大的元素的第一个位置
为什么left总会指向多个target中的第一个位置?
因为, 当nums[mid] == target的时候, 一定是右指针在动, 所以右指针总是比左指针先指向target, 并且当有多个target的时候, 右指针会一直向左缩, 直到指向第一个target
由于大部分语言的非符号整形是向下取整的, 所以最后左指针也一定指向这个位置, 左右指针索引相等,退出循环, 于是 left 等于第一个 target 的位置索引
下面演示一下target存在时的二分查找流程
target不存在时, 比如target = 4时, 最终left会指向第一个5
所以这样的if写法, 适用于实际工程情况, left总是能指向合适的位置
所以最终二分查找的通用模板为:
// 左闭右开
left, right := 0, len(nums)
for left < right{
// 中间点索引
mid := left + (right - left) >> 1
if nums[mid] >= target {
right = mid
}else{
left = mid + 1
}
}
具体实践
二分法
704. 二分查找
难度简单
1507
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
Code
func search(nums []int, target int) int {
// 左闭右开
left, right := 0, len(nums)
for left < right{
// 中间点索引
mid := left + (right - left) >> 1
if nums[mid] >= target {
right = mid
}else{
left = mid + 1
}
}
if left < len(nums) && nums[left] == target {
return left
}
return -1
}
性能如下
35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
直接套模板就行
Code
func search(nums []int, target int) int {
// 左闭右开
left, right := 0, len(nums)
for left < right{
// 中间点索引
mid := left + (right - left) >> 1
if nums[mid] >= target {
right = mid
}else{
left = mid + 1
}
}
if left < len(nums) && nums[left] == target {
return left
}
return -1
}
34. 在排序数组中查找元素的第一个和最后一个位置
Code
func searchRange(nums []int, target int) []int {
left_1, right_1 := 0, len(nums)
left_2, right_2 := 0, len(nums)
// 查找第一个位置
for left_1 < right_1 {
mid := left_1 + (right_1 - left_1) >> 1
if nums[mid] >= target {
right_1 = mid
}else {
left_1 = mid + 1
}
}
// 不存在target, 及时剪枝
if left_1 >= len(nums) || nums[left_1] != target {
return []int{-1, -1}
}
// 查找最后一个位置, 即left_2 - 1
for left_2 < right_2 {
mid := left_2 + (right_2 - left_2) >> 1
if nums[mid] >= target + 1 {
right_2 = mid
}else {
left_2 = mid + 1
}
}
return []int{left_1, left_2 - 1}
}
性能如下:
移除元素
具体实践
27. 移除元素
给你一个数组 nums **和一个值 val,你需要 原地 移除所有数值等于 val **的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
有两种做法:
- 暴力做法
- 快慢指针
暴力做法Code:
func removeElement(nums []int, val int) int {
n := len(nums)
for i :=n - 1;i >= 0; i--{
if nums[i] == val{
j := i
for j < n - 1{
nums[j] = nums[j + 1]
j ++
}
n --
}
}
return n
}
快慢指针Code:
func removeElement(nums []int, val int) int {
slow, fast := 0, 0
for ; fast < len(nums); fast++{
if nums[fast] != val{
nums[slow] = nums[fast]
slow ++
}
}
return slow
}
其实速度是一样快的