二分查找
每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O ( l o g n ) 。 O(logn)。 O(logn)。
举例来说,给定一个排好序的数组 {3,4,5,6,7},我们希望查找 4 在不在这个数组内。第一次折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一半。于是我们的查找区间变成了 {3,4,5}。(注意:根据具体情况和刷题习惯,这里的 5 可以保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数 4,正好是我们需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍历数组,最坏的情况则需要查找 5 次。
具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此
有些会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍:
- 第一是尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;
- 第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。
二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,
指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。
模板(采用左开右闭的写法)
[left, right] 区间找 == target 的值的位置
l,r = 0,len(ls) # 搜索区间左闭右开
def binarySearch(l,r,target):
while l < r:
mid = (l+r)//2
if ls[mid] > target:
r = mid
elif ls[mid] < target:
l = mid+1
else:
return mid
return -1
[left, right] 区间找 >= target 的最小值的位置
l,r = 0,len(ls)
def binarySearch(l,r,target):
while l<r:
mid = (l+r)//2
if ls[mid] >= target:
r = mid
else:
l = mid+1
return l
[left, right] 区间找 <= target 的最大值的位置
l,r = 0,len(ls)
def binarySearch(l,r,target):
while l<r:
mid = (l+r)//2
if ls[mid] > target:
r = mid
else:
l = mid+1
return l-1
例题
1. 根号2
l,r=1,2
def sqrt(l,r,eps):
while r-l > eps:
mid = (l+r)/2
if mid*mid <2:
l = mid
else:
r = mid
return l
2. 切木头
'''
将木棒至少分为k段时,木棒最长可以为多长
'''
k = 7
lengths = [10, 24, 15]
def cutWoods(left, right):
while left < right:
nums = 0
mid = (left+right)//2
for i in lengths:
nums += i//mid
if nums > k:
left = mid+1
else:
right = mid
return left
3.在排序数组中查找元素的第一个和最后一个位置(34)
题目描述:
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
def searchRange(self, nums: List[int], target: int) -> List[int]:
if target not in nums:return [-1,-1]
if len(nums) == 1:return [0,0]
l,r = 0,len(nums)
ans = []
while l<r:
mid = (l+r)//2
if nums[mid]>=target:
r = mid
else:
l = mid+1
ans.append(l)
l,r = 0,len(nums)
while l<r:
mid = (l+r)//2
if nums[mid]>target:
r = mid
else:
l = mid+1
ans.append(l-1)
return ans
4. 旋转数组查找数字(81)
题目描述:
一个原本增序的数组被首尾相连后按某个位置断开(如 [1,2,2,3,4,5] → [2,3,4,5,1,2],在第一
位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个为旋转数组
中。
解题思路:
对于有序数组,可以使用二分查找的方法查找元素。
但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分查找吗?答案是可以的。
可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例[4,5,6,7,0,1,2]来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。
这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
- 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [ n u m s [ l ] , n u m s [ m i d ] {nums}[l],{nums}[mid] nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
- 如果 [mid, r] 是有序数组,且 target 的大小满足 ( n u m s [ m i d + 1 ] , n u m s [ r ] {nums}[mid+1],{nums}[r] nums[mid+1],nums[r]]则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。
注意: 因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部
相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行
二分查找。
def search(self, nums: List[int], target: int) -> bool:
l, r = 0, len(nums)-1
while l<= r:
mid = (l+r) // 2
if nums[mid] == target:
return True
if nums[mid] > nums[l]:
if nums[l] <= target <nums[mid]:
r = mid -1
else:
l = mid + 1
elif nums[mid] == nums[l]:
l = l + 1
else:
if nums[mid] < target <=nums[r]:
l = mid + 1
else:
r = mid - 1
return False
5. 有序数组中的单一元素(540)
题目描述:
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
解题思路:
假设只出现一次的元素位于下标 x,由于其余每个元素都出现两次,因此下标 x 的左边和右边都有偶数/奇数个元素,数组的长度是奇数
由于数组是有序的,因此数组中相同的元素一定相邻
def singleNonDuplicate(self, nums: List[int]) -> int:
l, r = 0, len(nums) - 1
while l < r:
mid = (l+r) // 2
if nums[mid]==nums[mid^1]:
l = mid+1
else:
r = mid
return nums[l]
6.寻找两个正序数组的中位数(4)
题目描述:
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出并返回这两个正序数组的 中位数 。
解题思路:
中位数的定义,当
m
+
n
m+n
m+n 是奇数时,中位数是两个有序数组中的第
(
m
+
n
)
/
2
(m+n)/2
(m+n)/2 个元素,当
m
+
n
m+n
m+n 是偶数时,中位数是两个有序数组中的第
(
m
+
n
)
/
2
(m+n)/2
(m+n)/2 个元素和第
(
m
+
n
)
/
2
+
1
(m+n)/2+1
(m+n)/2+1 个元素的平均值。
因此,这道题可以转化成寻找两个有序数组中的第
k
k
k 小的数,其中
k
k
k 为
(
m
+
n
)
/
2
(m+n)/2
(m+n)/2 或
(
m
+
n
)
/
2
+
1
(m+n)/2+1
(m+n)/2+1。
假设两个有序数组分别是
A
和
B
A 和 B
A和B。要找到第
k
k
k 个元素,我们可以比较
A
[
k
/
2
−
1
]
A[k/2−1]
A[k/2−1] 和
B
[
k
/
2
−
1
]
B[k/2−1]
B[k/2−1],其中
/
/
/ 表示整数除法。
由于
A
[
k
/
2
−
1
]
A[k/2−1]
A[k/2−1] 和
B
[
k
/
2
−
1
]
B[k/2−1]
B[k/2−1] 的前面分别有
A
[
0..
k
/
2
−
2
]
A[0..k/2−2]
A[0..k/2−2] 和
B
[
0..
k
/
2
−
2
]
B[0..k/2−2]
B[0..k/2−2],即
k
/
2
−
1
k/2−1
k/2−1 个元素,对于
A
[
k
/
2
−
1
]
A[k/2−1]
A[k/2−1] 和
B
[
k
/
2
−
1
]
B[k/2−1]
B[k/2−1] 中的较小值,最多只会有
(
k
/
2
−
1
)
+
(
k
/
2
−
1
)
≤
k
−
2
(k/2−1)+(k/2−1)≤k−2
(k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第
k
k
k 小的数了。
因此我们可以归纳出两种情况:
-
如果 A [ k / 2 − 1 ] < = B [ k / 2 − 1 ] A[k/2−1]<=B[k/2−1] A[k/2−1]<=B[k/2−1]
则比 A [ k / 2 − 1 ] A[k/2−1] A[k/2−1] 小的数最多只有 A A A 的前 k / 2 − 1 k/2-1 k/2−1 个数和 B B B 的前 k / 2 − 1 k/2−1 k/2−1 个数,即比 A [ k / 2 − 1 ] A[k/2−1] A[k/2−1] 小的数最多只有 k − 2 k−2 k−2 个,因此 A [ k / 2 − 1 ] A[k/2−1] A[k/2−1] 不可能是第 k k k 个数, A [ 0 ] A[0] A[0]到 A [ k / 2 − 1 ] A[k/2−1] A[k/2−1] 也都不可能是第 k k k 个数,可以全部排除。 -
如果 A [ k / 2 − 1 ] > B [ k / 2 − 1 ] A[k/2−1]>B[k/2−1] A[k/2−1]>B[k/2−1]
则可以排除 B [ 0 ] B[0] B[0] 到 B [ k / 2 − 1 ] B[k/2−1] B[k/2−1]。
可以看到,比较
A
[
k
/
2
−
1
]
A[k/2−1]
A[k/2−1] 和
B
[
k
/
2
−
1
]
B[k/2−1]
B[k/2−1] 之后,可以排除
k
/
2
k/2
k/2 个不可能是第
k
k
k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少
k
k
k 的值,这是因为我们排除的数都不大于第
k
k
k 小的数。
三种情况需要特殊处理:
- 如果 A [ k / 2 − 1 ] A[k/2−1] A[k/2−1] 或者 B [ k / 2 − 1 ] B[k/2−1] B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k k k 的值,而不能直接将 k k k 减去 k / 2 k/2 k/2。
- 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k k k 小的元素。
- 如果 k = 1 k=1 k=1,我们只要返回两个数组首元素的最小值即可。
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
def getKthElement(k):
idx1,idx2 = 0,0
while 1:
if idx1 == m:
return nums2[idx2+k-1]
if idx2 == n:
return nums1[idx1+k-1]
if k == 1:
return min(nums1[idx1],nums2[idx2])
newIdx1 = min(idx1+ k//2 -1,m-1)
newIdx2 = min(idx2+k//2-1,n-1)
pivot1,pivot2 = nums1[newIdx1],nums2[newIdx2]
if pivot1<=pivot2:
k -= newIdx1-idx1+1
idx1 = newIdx1+1
else:
k -= newIdx2-idx2+1
idx2 = newIdx2+1
m,n = len(nums1),len(nums2)
totalLength = m + n
if totalLength % 2 == 1:
return getKthElement((totalLength + 1) // 2)
else:
return (getKthElement(totalLength // 2) + getKthElement(totalLength // 2 + 1)) / 2
7. P3382 三分
# p3382 三分法
def f(x):
res = 0
#秦九韶算法 将一元n次多项式的求值问题转化为n个一次式的算法,比普通计算方式提高了一个数量级
for i in range(n + 1):
res = res * x + ls[i]
return res
n, l, r = map(eval, input().split())
ls = list(map(eval, input().split())) # 系数
while r - l > 1e-6:
mid = (r + l) / 2
mid1 = mid - 1e-5
mid2 = mid + 1e-5
if f(mid1) > f(mid):
r = mid
elif f(mid2) > f(mid):
l = mid
else:
print("{:.5f}".format(mid))
break
#print(mid)
'''
第二种
while r - l > 1e-6:
d = (r - l) / 3
mid1 = l + d
mid2 = r - d
if f(mid1) > f(mid2):
r = mid2
else:
l = mid1
print("{:.5f}".format(l))
'''