二分查找
最后更新于
这有帮助吗?
针对有序数据集合的查找算法:二分查找(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]
是不是第一个值等于给定值的元素。
看第 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
看第7、8行代码:
如果list[mid]>=value
,且mid==0
,那就是要查找的值
如果list[mid]
的前一个值小于value
,那就是要查找的值
否则说明要查找的值在[low,mid-1]
之间
如果list[mid]<value
,说明要查找的之在[mid+1,high]
之间
实际上,求“值等于给定值”的二分查找确实不怎么会被用到,二分查找更适合用在“近似”查找问题,在这类问题上,二分查找的优势更加明显。
上述这些变体的二分查找算法很容易因为细节处理不好而产生 Bug,这些容易出错的细节有:
终止条件
区间上下界更新方法
返回值选择
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
经典二分查找应用场景,注意:
退出条件是 start>end,也就是start<=end的情况都满足条件(只要一个值的时候也可以比较)
因为mid值已经比较过不用再考虑,所以start和end值更新为mid+1和mid-1
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
可以假设数组中无重复元素。
要找到待插入位置,其实就是寻找对个大于等于taget值的位置。
当都没有找到的时候,说明target值比数组中的值都大,那么就在末尾插入。
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。
根据附加条件,可以将二维数组转换为一维有序数组,然后使用二分查找,关键点就是如何计算mid值在二位数组中对应的坐标:
col表示二位数组中每一行的长度:
i:= mid/col j:= mid%col
通过上面的转换,就可以将mid对应的值和target进行比较。
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
查找有序数组中第一个大于等于目标值的索引。
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
因为是在中间的某个点进行了旋转,所以将数组最后一个元素作为目标值,然后要在数组中遵照最后一个小于等于它的元素,当循环退出时,start在end之前,所以start就是我们要找的结果。
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。
与上一题一样,关键是数组中存储重复的元素,在进行二分查找之前,现将重复的元素去除。
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。