二分查找刷题总结
二分查找原理
特点:
- 输入是个有序的元素列表
- 要查找的元素包含在列表中,二分查找返回其位置
- 对于包含n个元素的列表,用二分查找最多需要log2 n步
原理:
首先定义两个变量指向有序列表的头(low)和尾(high),然后每次都检查中间的元素。如果猜小了,就修改low;如果猜大了,就修改high。
def binary_search(list, item):
low = 0
high = len(list) - 1
while low <= high:
mid = (low + high) / 2 #如果不是偶数,python将自动向下取整
guess = list[mid]
if guess == item:
return mid
if guess < item:
low = mid + 1
if guess > item:
high = mid - 1
return None
三个模板总结:
// 二分查找 --- [left, right]
// 数组已经是有序的了!
public static int binarySerach1(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0, right = nums.length-1;
while (left <= right) {
// 防止溢出 等同于(left + right)/2
int mid = left + (right-left)/2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
// target 在左区间,所以[left, middle - 1]
right = mid-1;
} else {
// target 在右区间,所以[middle + 1, right]
left = mid+1;
}
}
return -1;
}
// 二分查找 --- [left, right)
// 数组已经是有序的了!
int binarySearch2(int[] nums, int target){
if(nums == null || nums.length == 0)
return -1;
// 定义target在左闭右开的区间里,即:[left, right)
int left = 0, right = nums.length;
// 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] == target){
return mid;
}
else if(nums[mid] < target) {
// target 在右区间,在[middle + 1, right)中
left = mid + 1;
}
else {
// target 在左区间,在[left, middle)中
right = mid;
}
}
// Post-processing:
// End Condition: left == right
if(left != nums.length && nums[left] == target) return left;
return -1;
}
// 二分查找 --- (left, right)
// 数组已经是有序的了!
int binarySearch3(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
int left = 0, right = nums.length - 1;
while (left + 1 < right){
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
// target 在右区间,在(middle, right)中
left = mid;
} else {
// target 在左区间,在(left, middle)中
right = mid;
}
}
// Post-processing:
// End Condition: left + 1 == right
if(nums[left] == target) return left;
if(nums[right] == target) return right;
return -1;
}
刷题经验:
-
二分查找的核心就是找到具有单调性的正确查找对象
-
当遇到指针不知道往哪移动的情况时,将二分查找退为普通遍历
-
对于有序数组,二分算法是最快的
-
选择二分查找方法,最重要的是要找到二分的分界,即分成了哪两个部分呢?
-
当数据规模达到 \(10^5\) 以上时,考虑使用二分算法更快
-
当二分查找的对象不在给定的数组中时,在答案的可能取值区间,进行二分查找
-
涉及到大范围\(10^9\)、第K个最大最小,K个数量这种很多都是二分解法
二分查找刷题
1. 69题:x的平方根--普通二分查找
- 题目描述:
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留整数部分 ,小数部分将被舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
- 题目示例
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
- 题目分析
给定的是非负整数 X ,可以把这个整数转换成一个有序的 0 ~ X的数组,这样就可以使用二分法来快速找到它的算术平方根了。由于是取下届作为X的算术平方根, 我们可以将每次遍历到\(mid *mid <x\)的位置存下来,这样当循环终止的时候,\(ans\) 总是停留在最接近X算术平方根的位置。
- 算法实现
class Solution:
def mySqrt(self, x: int) -> int:
l, r, ans = 0, x, -1
while l <= r:
mid = (l + r) // 2
if mid * mid <= x:
ans = mid
l = mid + 1
else:
r = mid - 1
return ans
- 复杂度分析
- 时间复杂度:\(O(logn)\)
- 空间复杂度: \(O(1)\)
2. 1760. 袋子里最少数目的球 --在答案的可能取值区间进行二分查找
-
题目描述:
给你一个整数数组 \(nums\) ,其中 \(nums[i]\) 表示第 i 个袋子里球的数目。同时给你一个整数 \(maxOperations\) 。
你可以进行如下操作至多 \(maxOperations\) 次:
选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。
你的开销是单个袋子里球数目的最大值 ,你想要最小化开销。请你返回进行上述操作后的最小开销。
-
题目示例:
输入:nums = [2,4,8,2], maxOperations = 4
输出:2
解释:
-将装有 8 个球的袋子分成装有 4 个和 4 个球的袋子。[2,4,8,2] -> [2,4,4,4,2] 。
-将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,4,4,4,2] -> [2,2,2,4,4,2] 。
-将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,4,4,2] -> [2,2,2,2,2,4,2] 。
-将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,2,2,4,2] -> [2,2,2,2,2,2,2,2] 。
装有最多球的袋子里装有 2 个球,所以开销为 2 并返回 2 。
-
数据大小:
\(1 <= nums.length <= 10^5\)
\(1 <= maxOperations, nums[i] <= 10^9\) -
题目分析:
我们无法在nums数组中进行二分查找找到答案,所以要考虑如何求得\(maxOperations\)呢?
首先转换成判定问题,即:给定 \(maxOperations\)次操作次数,能否可以使得单个袋子里球数目的最大值不超过 y?
由于当 y 增加时,操作次数会减少,因此 y 具有单调性,我们可以通过二分查找的方式得到答案。
事实上,如果单个袋子里有 x 个球,那么操作次数即为:\[\lfloor \frac{x-1}{y} \rfloor \]其中 \(\lfloor x \rfloor\)表示将 x 进行下取整。因此我们需要找到最小的 y,使得:
\[\sum_{x \in nums} \lfloor \frac{x-1}{y} \rfloor \leq maxOperations \]成立。
-
算法实现:
class Solution(object):
def minimumSize(self, nums, maxOperations):
"""
:type nums: List[int]
:type maxOperations: int
:rtype: int
"""
# 事先不知道答案,在答案的可能取值区间,进行二分查找
left = 1
right = max(nums)
#print(right)
ans = 0
while left <= right:
y = (left + right) // 2
ops = 0
for num in nums:
ops += (num - 1) // y
if ops <= maxOperations:
ans = y
right = y - 1
else:
left = y + 1
return ans
- 复杂度分析:
- 时间复杂度:\(O(nlog\space n)\)
- 空间复杂度:\(O(1)\)
2. 436.寻找右区间
-
问题描述:
给你一个区间数组 intervals ,其中 \(intervals[i] = [start_i, end_i]\) ,且每个 \(start_i\) 都 不同 。区间 i 的右侧区间可以记作区间 j ,并满足 \(start_j >= end_i\) ,且 \(start_j\) 最小化 。
返回一个由每个区间 i 的 右侧区间 的最小起始位置组成的数组。如果某个区间 i 不存在对应的 右侧区间 ,则下标 i 处的值设为 -1 。
-
示例:
输入:intervals = [[3,4],[2,3],[1,2]]
输出:[-1,0,1]
解释:对于 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间[3,4]具有最小的“右”起点;
对于 [1,2] ,区间[2,3]具有最小的“右”起点。
- 方法一:二分查找
对区间 \(intervals\) 的起始位置进行排序,并将每个起始位置 \(intervals[i][0]\)对应的索引 i 存储在数组 \(startIntervals\) 中,然后枚举每个区间 i 的右端点 \(intervals[i][1]\),利用二分查找来找到大于等于 \(intervals[i][1]\) 的最小值 \(val\) 即可,此时区间 i 对应的右侧区间即为右端点 \(val\) 对应的索引。 - 算法实现
class Solution:
def findRightInterval(self, intervals: List[List[int]]) -> List[int]:
for i, interval in enumerate(intervals):
interval.append(i)
intervals.sort()
n = len(intervals)
ans = [-1] * n
for _, end, id in intervals:
i = bisect_left(intervals, [end])
if i < n:
ans[id] = intervals[i][2]
return ans
- 复杂度分析
时间复杂度:O(nlogn),其中 nn 为区间数组的长度。排序的时间为 O(nlogn),每次进行二分查找花费的时间为 O(logn),一共需要进行 nn 次二分查找,因此总的时间复杂度为 O(nlogn)。
空间复杂度:O(n),其中 n 为区间数组的长度。startIntervals 一共存储了 n 个元素,因此空间复杂度为 O(n)。

浙公网安备 33010602011771号