定义
在一个有序数组中,以O(logn)的时间复杂度找到指定的元素的算法,称之为二分查找。
标准实现
当数组中的元素为严格递增时,标准实现如下
func binarySearch(nums []int, target int) int {
low, high := 0, len(nums)-1
for low <= high {
mid := low + ((high-low)>>1)
if nums[mid] == target {
return mid
} else if nums[mid] < target {
low = mid + 1
} else {
high = mid - 1
}
}
return -1
}
- 结束条件:low > high
mid=(mid+high)/2为了避免加法溢出,优化为mid=low+(high-low)/2;为了进一步提高性能,可以继续优化为mid=low+((high-low)>>1)- 查找不到元素时返回-1
变体
当数组不是严格递增(或者递减)的时候,即存在相同的元素,会有以下4种常见的查询需求
找到第一个等于target的元素
func binarySearch(nums []int, target int) int {
low, high := 0, len(nums)-1
for low <= high {
mid := low + ((high-low)>>1)
if nums[mid] == target {
if mid == 0 || nums[mid-1] != target {
return mid
}
high = mid - 1
} else if nums[mid] < target {
low = mid + 1
} else {
high = mid - 1
}
}
return -1
}
找到最后一个等于target的元素
func binarySearch(nums []int, target int) int {
low, high := 0, len(nums)-1
for low <= high {
mid := low + ((high-low)>>1)
if nums[mid] == target {
if mid == len(nums)-1 || nums[mid+1] != target {
return mid
}
low = mid + 1
} else if nums[mid] < target {
low = mid + 1
} else {
high = mid - 1
}
}
return -1
}
找到第一个大于等于target的元素
func binarySearch(nums []int, target int) int {
low, high := 0, len(nums)-1
for low <= high {
mid := low + ((high-low)>>1)
if nums[mid] >= target {
if mid == 0 || nums[mid-1] < target {
return mid
}
high = mid - 1
} else {
low = mid + 1
}
}
return -1
}
找到最后一个小于等于target的元素
func binarySearch(nums []int, target int) int {
low, high := 0, len(nums)-1
for low <= high {
mid := low + ((high-low)>>1)
if nums[mid] <= target {
if mid == len(nums)-1 || nums[mid+1] > target {
return mid
}
low = mid + 1
} else {
high = mid -1
}
}
return -1
}
拓展问题
二分查找可以用递归实现吗?
可以,以标准实现为例
- 递归公式
bs(target, low, high) = mid || bs(target, low, mid-1) || bs(target, mid+1, high)
- 结束条件
low > high
- 翻译成代码
func binarySearch(nums []int, target int) int {
low, high := 0, len(nums)-1
return bs(nums, target, low, high)
}
func bs(nums []int, target, low, high int) int {
if low > high {
return -1
}
mid := low + ((high-low)>>1)
if nums[mid] == target {
reutrn mid
} else if nums[min] < target {
return bs(nums, target, mid+1, high)
} else {
return bs(nums, target, low, mid-1)
}
}
链表是否也能使用二分查找?
链表不支持随机访问,即使链表数据是有序的,也不能使用二分查找,只能从头到尾遍历,时间复杂度为O(n)。
但是我们可以对链表进行索引改造,使其查询效率达到O(logn),这种数据结构称为跳表。不过有得有失,虽然查询效率上来了,但是为了维护索引平衡性,插入删除元素的时间复杂度会从O(1)下降到O(logn),同时也需要额外的空间来存储索引。