二分查找,它是基于分治策略的高效搜索算法,利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。
优势和局限:二分查找仅适用于有序数据的数组(需跳跃式(非连续地)访问元);其时间复杂度通常为
O
(
l
o
g
n
)
O(\mathrm{log} n)
O(logn)。
场景1:给定一个数组 nums
,其数组没有重复元素且元素按从小到大排序,数组长度为 n
;在数组查找元素 target
并返回对应的下标,若数组不包含目标元素,就返回
−
1
-1
−1。
算法流程:
(1)初始化两个指针 i=0
和 j=n-1
,分别指向数组的首元素和尾元素,代表搜索区间
[
0
,
n
−
1
]
[0, n-1]
[0,n−1]。此时我们规定:下标严格小于 i
的元素一定小于 target
,而下标严格大于 j
的元素一定大于 target
。
(2)满足 i <= j
的条件下,循环执行以下两步:
(2.1)计算中点索引
m
=
⌊
(
i
+
j
)
/
2
⌋
m = \lfloor(i+j)/2\rfloor
m=⌊(i+j)/2⌋;
(2.2)判断 nums[m]
和 target
的大小关系,分为以下三种情况:
a、当 nums[m] = target
时,说明找到 target
,因此返回索引 m
;
b、当 nums[m] < target
时,说明 target
在区间
[
m
+
1
,
j
]
[m+1, j]
[m+1,j] 中,因此执行
i
=
m
+
1
i = m + 1
i=m+1;
c、当 nums[m] > target
时,说明 target
在区间
[
i
,
m
−
1
]
[i, m-1]
[i,m−1] 中,因此执行
j
=
m
−
1
j = m - 1
j=m−1。
在这个过程中,区间大小会不断减少,知道遇到目标元素或者为空(
i > j
),结束循环,查找结束。
情况 b 中,修改指针i
的取值,说明区间 [ 0 , i − 1 ] [0, i-1] [0,i−1] 的元素均小于target
(此时 i = m + 1 i=m+1 i=m+1),满足上述规定;
情况 c 中,修改指针j
的取值,说明区间 [ j + 1 , n − 1 ] [j+1, n-1] [j+1,n−1] 的元素均大于target
(此时 j = m − 1 j=m-1 j=m−1),满足上述规定;
当出现i = j
时,说明待判断的元素只剩下一个:若是目标值,则直接返回下标,若不是目标值,根据其与target
关系修改指针的值,进而查找范围为空。
def binarySearch(nums, target):
n = len(nums)
i, j = 0, n-1
while i <= j:
m = i + (j-i)//2
if nums[m] < target:
i = m+1
elif nums[m] > target:
j = m-1
else:
return m
return -1
场景2:在上述数组的基础上,假设数组中存在多个 target
,则上述的二分查找只能返回其中一个的索引,无法确定该元素的左边和右边还有多少 target
。
为此,扩展上面的二分查找代码,使其能找到第一个 target
,并返回对应的下标。
算法流程:整体流程保持不变,初始化 i=0
和 j=n-1
;接着,每轮循环中计算索引
m
m
m,再判断 nums[m]
和 target
的关系:
(1) 当 nums[m] > target
或 nums[m] < target
时,说明还没有找到目标值,则跟上述一样,修改指针 i
和 j
,使其缩小范围;
(2) 当 nums[m] == target
时,说明找到目标元素,可采用
j
=
m
−
1
j=m-1
j=m−1 来缩小搜索区间,这可保持小于 target 的元素在
[
0
,
m
−
1
]
[0, m-1]
[0,m−1] 中,而指针 j
向着小于 target
元素靠近;
完成循环后,i
指向最左边的 target
,而 j
指向首个小于 target
的元素。最终,返回索引 i
。
在这个过程中规定:下标小于
i
的元素一定小于target
,而下标大于j
的元素是大于等于target
。
def binarySearch2(nums, target):
"""二分查找(存在重复元素)"""
n = len(nums)
i, j = 0, n-1
while i <= j:
m = i + (j-i)//2
if nums[m] < target:
i = m+1
elif nums[m] > target:
j = m-1
else:
j = m-1
return i
参考:《Hello 算法》