二分查找
针对有序数据集合的查找算法:二分查找(Binary Search)算法,也叫折半查找算法。
假设有10个数据:8,11,19,23,27,33,45,55,67,98,快速找到19这个值是否在数据中。
利用二分思想,每次都与区间的中间数据比对大小,缩小查找区间的范围,过程如下图所示,其中,low
和high
表示待查找区间的下标,mid
表示待查找区间的中间元素下标。

二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。
每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。
时间复杂度
假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。

这是一个等比数列
其中 时,k 的值就是总共缩小的次数
每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度是
O(k)
通过 ,求得 ,时间复杂度是
O(logn)
O(logn)
这种对数时间复杂度,是一种极其高效的时间复杂度,有时甚至比时间复杂度是常量级O(1)
的算法还要高效。
最简单情况的代码实现
最简单的情况就是有序数组中不存在重复元素。
创建三个变量
low
、high
、mid
都是数组下标其中
low
和high
表示当前查找的区间范围,初始low=0
,high=n-1
mid
表示[low, high]
的中间位置通过对比
a[mid]
与value
的大小,来更新接下来要查找的区间范围,直到找到或者区间缩小为0
,就退出
注意:
循环退出条件:是
low>=high
,而不是low>igh
mid
的取值:在数据较大时可能溢出,改成,优化性能可以将除法运算改为位运算low+((high-low)>>1)
low
和high
的更新:low=mid+1
,high=mid-1
,如果直接写成low=mid
或者high=mid
,就可能会发生死循环
也可以使用递归实现:
递推公式:
binarySearch(p...r,value)=binarySearch(p...mid-1,value) or binarySearch(mid+1...r,value)
终止条件:
A[mid] = value or low >= high
应用场景的局限性
二分查找依赖的是顺序表结构(简单点说就是数组):因为二分查找算法需要按照下标随机访问元素
二分查找针对的是有序数据:如果数据没有序,需要先排序,排序的时间复杂度最低是
O(nlogn)
。所以,如果针对一组静态数据(没有频繁地插入、删除),可以进行一次排序,多次二分查找。这样排序的成本可被均摊,二分查找的边际成本就会比较低数据量太小不适合二分查找:数据量太小,遍历数组就足够(如果数据之间的比较操作非常耗时,需要尽可能减少比较次数,推荐使用二分查找)
数据量太大也不适合二分查找:底层依赖数组,数组实现随机访问需要内存中连续的空间,这个条件比较苛刻,所以太大的数据用数组存储就比较吃力,就不能用二分查找
常见变形问题的代码实现
有序数据集合中不存在重复的数据。
查找第一个值等于给定值的元素
a[mid]
跟要查找的 value
的大小关系有三种情况:
对于
a[mid]>value
的情况,更新high=mid-1
对于
a[mid]<value
的情况,更新low=mid+1
对于
a[mid]=value
的情况如果查找的是任意一个值等于给定值的元素,当
a[mid]
等于要查找的值时,a[mid]
就是要找的元素。如果查找的是第一个值等于给定值的元素,当
a[mid]
等于要查找的值时,就需要确认一下这个a[mid]
是不是第一个值等于给定值的元素。
func BinarySearchFirst(list []int, value int) int {
low := 0
high := len(list) - 1
for low <= high {
mid := low + ((high - low) >> 1)
if list[mid] < value {
low = mid + 1
} else if list[mid] > value {
high = mid - 1
} else {
if mid == 0 || list[mid-1] != value {
return mid
} else {
high = mid - 1
}
}
}
return -1
}
看第 12 行代码。
如果 mid 等于 0,那这个元素已经是数组的第一个元素;
如果 mid 不等于 0,但
a[mid]
的前一个元素a[mid-1]
不等于 value,那也说明a[mid]
就是要找的第一个值等于给定值的元素。如果经过检查之后发现
a[mid]
前面的一个元素a[mid-1]
也等于 value,那说明此时的a[mid]
肯定不是要查找的第一个值等于给定值的元素。更新high=mid-1
,要找的元素肯定出现在[low, mid-1]
之间。
查找最后一个值等于给定值的元素
基于上面的分析:
将第12行代码中
if
的判定条件修改为mid == len(a)-1 || list[mid+1] != value
将第15行代码修改为
low=mid+1
查找第一个大于等于给定值的元素
func BinarySearchGE(list []int, value int) int {
length := len(list)
low := 0
high := length - 1
for low <= high {
mid := low + ((high - low) >> 1)
if list[mid] >= value {
if mid == 0 || list[mid-1] < value {
return mid
} else {
high = mid - 1
}
} else {
low = mid + 1
}
}
return -1
}
看第7、8行代码:
如果
list[mid]>=value
,且mid==0
,那就是要查找的值如果
list[mid]
的前一个值小于value
,那就是要查找的值否则说明要查找的值在
[low,mid-1]
之间如果
list[mid]<value
,说明要查找的之在[mid+1,high]
之间
查找最后一个小于等于给定值的元素
func BinarySearchLE(list []int, value int) int {
length := len(list)
low := 0
high := length - 1
for low <= high {
mid := low + ((high - low) >> 1)
if list[mid] > value {
high = mid - 1
} else {
if mid == len(list)-1 || list[mid+1] > value {
return mid
} else {
low = mid + 1
}
}
}
return -1
}
实际上,求“值等于给定值”的二分查找确实不怎么会被用到,二分查找更适合用在“近似”查找问题,在这类问题上,二分查找的优势更加明显。
上述这些变体的二分查找算法很容易因为细节处理不好而产生 Bug,这些容易出错的细节有:
终止条件
区间上下界更新方法
返回值选择
常考考点
二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
思路
经典二分查找应用场景,注意:
退出条件是 start>end,也就是start<=end的情况都满足条件(只要一个值的时候也可以比较)
因为mid值已经比较过不用再考虑,所以start和end值更新为mid+1和mid-1
示例代码
func search(nums []int, target int) int {
n := len(nums)
if n < 1 {
return -1
}
start, end := 0, n-1
for start <= end {
mid := start + (end-start)/2
if nums[mid] < target {
start = mid + 1
} else if nums[mid] > target {
end = mid - 1
} else {
return mid
}
}
return -1
}
搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
可以假设数组中无重复元素。
思路
要找到待插入位置,其实就是寻找对个大于等于taget值的位置。
当都没有找到的时候,说明target值比数组中的值都大,那么就在末尾插入。
示例代码
func searchInsert(nums []int, target int) int {
n := len(nums)
if n < 1 {
return -1
}
start, end := 0, n-1
for start <= end {
mid := start + (end-start)/2
if nums[mid] >= target {
if mid == 0 || nums[mid-1] < target {
return mid
} else {
end = mid - 1
}
} else {
start = mid + 1
}
}
return n
}
搜索二维矩阵
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。
思路
根据附加条件,可以将二维数组转换为一维有序数组,然后使用二分查找,关键点就是如何计算mid值在二位数组中对应的坐标:
col表示二位数组中每一行的长度:
i:= mid/col j:= mid%col
通过上面的转换,就可以将mid对应的值和target进行比较。
示例代码
func searchMatrix(matrix [][]int, target int) bool {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return false
}
row := len(matrix)
col := len(matrix[0])
start, end := 0, row*col-1
for start <= end {
mid := start + (end-start)/2
// 求mid在二维数组中对应的坐标
val := matrix[mid/col][mid%col]
if val > target {
end = mid - 1
} else if val < target {
start = mid + 1
} else {
return true
}
}
return false
}
第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
思路
查找有序数组中第一个大于等于目标值的索引。
示例代码
func firstBadVersion(n int) int {
start := 0
end := n
for start <= end {
mid := start + (end-start)/2
if isBadVersion(mid) {
// 当前版本能够成功
// 那么查看前一个版本能否成功
if !isBadVersion(mid - 1) {
return mid
} else {
end = mid - 1
}
} else {
start = mid + 1
}
}
return -1
}
寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
思路
因为是在中间的某个点进行了旋转,所以将数组最后一个元素作为目标值,然后要在数组中遵照最后一个小于等于它的元素,当循环退出时,start在end之前,所以start就是我们要找的结果。
示例代码
func findMin(nums []int) int {
n := len(nums)
if n == 0 {
return -1
}
target := nums[n-1]
start, end := 0, n-1
for start <= end {
mid := start + (end - start)
if nums[mid] <= target {
end = mid - 1
} else {
start = mid + 1
}
}
return nums[start]
}
寻找旋转排序数组中的最小值【变体1】
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。
思路
与上一题一样,关键是数组中存储重复的元素,在进行二分查找之前,现将重复的元素去除。
示例代码
func findMin(nums []int) int {
n := len(nums)
if n == 0 {
return -1
}
start, end := 0, n-1
for start+1 < end {
for start < end && nums[end] == nums[end-1] {
end--
}
for start < end && nums[start] == nums[start+1] {
start++
}
mid := start + (end-start)/2
if nums[mid] <= nums[end] {
end = mid
} else {
start = mid
}
}
if nums[start] > nums[end] {
return nums[end]
}
return nums[start]
}
搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
思路
示例代码
最后更新于
这有帮助吗?