搜索--二分查找
二分查找,它是基于分治策略的高效搜索算法,利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。
优势和局限:二分查找仅适用于有序数据的数组(需跳跃式(非连续地)访问元);其时间复杂度通常为 O(logn)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\rfloorm=⌊(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+1i = m + 1i=m+1;
c、当 nums[m] > target
时,说明 target
在区间 [i,m−1][i, m-1][i,m−1] 中,因此执行 j=m−1j = m - 1j=m−1。
在这个过程中,区间大小会不断减少,知道遇到目标元素或者为空(
i > j
),结束循环,查找结束。
情况 b 中,修改指针i
的取值,说明区间 [0,i−1][0, i-1][0,i−1] 的元素均小于target
(此时 i=m+1i=m+1i=m+1),满足上述规定;
情况 c 中,修改指针j
的取值,说明区间 [j+1,n−1][j+1, n-1][j+1,n−1] 的元素均大于target
(此时 j=m−1j=m-1j=m−1),满足上述规定;
当出现i = j
时,说明待判断的元素只剩下一个:若是目标值,则直接返回下标,若不是目标值,根据其与target
关系修改指针的值,进而查找范围为空。
def binarySearch(nums, target):n = len(nums)i, j = 0, n-1while i <= j:m = i + (j-i)//2if nums[m] < target:i = m+1elif nums[m] > target:j = m-1else:return mreturn -1
场景2:在上述数组的基础上,假设数组中存在多个 target
,则上述的二分查找只能返回其中一个的索引,无法确定该元素的左边和右边还有多少 target
。
为此,扩展上面的二分查找代码,使其能找到第一个 target
,并返回对应的下标。
算法流程:整体流程保持不变,初始化 i=0
和 j=n-1
;接着,每轮循环中计算索引 mmm,再判断 nums[m]
和 target
的关系:
(1) 当 nums[m] > target
或 nums[m] < target
时,说明还没有找到目标值,则跟上述一样,修改指针 i
和 j
,使其缩小范围;
(2) 当 nums[m] == target
时,说明找到目标元素,可采用 j=m−1j=m-1j=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-1while i <= j:m = i + (j-i)//2if nums[m] < target:i = m+1elif nums[m] > target:j = m-1else:j = m-1return i
参考:《Hello 算法》