欢迎大家订阅【蓝桥杯Python每日一练】 专栏,开启你的 Python数据结构与算法 学习之旅!
1 二分查找
①定义
在计算机科学中,二分算法(Binary Search)是一种高效的查找算法,用于在一个已排序的数组中查找某个目标元素。二分算法的基本思想是通过将问题规模每次减半,来缩小查找范围,从而实现高效搜索。
②原理
二分查找算法基于分治思想。给定一个已排序的数组,二分算法通过不断将数组分为两部分,比较目标值与中间元素的关系,来确定目标值所在的范围。
③算法步骤
- 初始化边界:设定两个指针,
left
和right
,分别指向数组的起始和结束位置。 - 计算中间位置:在每次查找中,计算中间位置
mid = (left + right) // 2
。 - 比较目标与中间值:
- 如果目标值等于中间元素
array[mid]
,则查找成功,返回该元素的索引。 - 如果目标值小于中间元素,目标元素只能在左半部分,因此将右边界
right
更新为mid - 1
。 - 如果目标值大于中间元素,目标元素只能在右半部分,因此将左边界
left
更新为mid + 1
。
- 如果目标值等于中间元素
- 终止条件:当
left
>right
时,查找失败,目标元素不在数组中。
④时间复杂度分析
二分查找算法的时间复杂度为 O(log n),其中 n
是数组的元素个数。原因在于,每次查找时都会将搜索区间减半。因此,二分查找的效率非常高,尤其适用于大规模的数据集。
⑤空间复杂度分析
- 非递归实现的空间复杂度为 O(1),因为只使用了常数级的额外空间。
- 递归实现的空间复杂度为 O(log n),因为递归调用会占用栈空间。
⑤应用场景
二分查找算法在许多实际问题中都有广泛的应用,尤其是当数据集已经排好序时。常见的应用场景包括:
- 查找数据:二分查找常用于在已排序的数组中查找元素,尤其是当元素数量很大时,二分查找能够显著提高查找效率。
- 插入位置:在一些算法中,我们需要找出某个元素在一个已排序数组中的插入位置,这时二分查找是一种高效的方法。
- 区间问题:二分查找还可以用来解决一些特殊的区间问题,比如寻找满足某个条件的最小值或最大值。
- 搜索引擎与数据库:搜索引擎和数据库在对大量有序数据进行检索时,通常会用到二分查找。
2 例题分析
题目地址:https://www.lanqiao.cn/problems/99/learning/
样例输入
2 10
6 5
5 6
样例输出
2
【示例代码】
# 输入巧克力块的数量N和需要切出的块的数量K
N, K = map(int, input().split())
a = []
for i in range(N):
# 输入每个巧克力的长宽
x, y = map(int, input().split())
# 将巧克力的长宽作为元组存储到列表a中
a.append((x, y))
# check函数:判断给定边长x时,是否能够切出至少K块巧克力小块
def check(x):
# cnt表示当前边长x下,能够切出的块的总数
cnt = 0
# 遍历每一块巧克力
for H, W in a:
# 计算当前巧克力中可以切出多少个边长为x的小块
cnt += (H // x) * (W // x)
# 如果切出的块数大于等于K,则返回True,表示可能
return cnt >= K
# 二分查找边长x的最大值
left, right = 1, 100000 # 边长x的搜索范围,从1到100000
ans = 1 # 初始假定最小的有效边长为1
while left <= right:
mid = (left + right) // 2 # 取当前搜索区间的中点
if check(mid): # 如果可以切出至少K块小块
ans = mid # 记录当前中点作为可能的答案
# 尝试寻找更大的边长,调整搜索区间为[mid+1, right]
left = mid + 1
else:
# 否则尝试更小的边长,调整搜索区间为[left, mid-1]
right = mid - 1
print(ans) # 输出最大边长
【执行流程】
①初始输入
N, K = map(int, input().split()) # 输入巧克力块的数量N和需要切出的块的数量K
a = []
for i in range(N):
# 输入每个巧克力的长宽
x, y = map(int, input().split())
# 将巧克力的长宽作为元组存储到列表a中
a.append((x, y))
- 输入的
2 10
解析为N = 2
和K = 10
,表示有 2 块巧克力,目标是切出 10 块小巧克力。 - 接下来,输入两行表示巧克力的尺寸:
6 5
和5 6
。 a
列表最终存储为[(6, 5), (5, 6)]
,即两块巧克力的尺寸。
②定义 check(x)
函数
def check(x):
# cnt表示当前边长x下,能够切出的块的总数
cnt = 0
# 遍历每一块巧克力
for H, W in a:
# 计算当前巧克力中可以切出多少个边长为x的小块
cnt += (H // x) * (W // x)
# 如果切出的块数大于等于K,则返回True,表示可能
return cnt >= K
check(x)
的作用是,给定边长x
,判断是否能够切出至少K
块小巧克力。- 对于每一块巧克力
(H, W)
,计算在该巧克力中能够切出的边长为x
的小块数目为(H // x) * (W // x)
,即在高度和宽度上都切割出多少个小块。 - 最后,返回
cnt >= K
,表示切出的块数是否达到或超过了K
。
③二分查找最大边长
left, right = 1, 100000 # 边长x的搜索范围,从1到100000
ans = 1 # 初始假定最小的有效边长为1
while left <= right:
mid = (left + right) // 2 # 取当前搜索区间的中点
if check(mid): # 如果可以切出至少K块小块
ans = mid # 记录当前中点作为可能的答案
# 尝试寻找更大的边长,调整搜索区间为[mid+1, right]
left = mid + 1
else:
# 否则尝试更小的边长,调整搜索区间为[left, mid-1]
right = mid - 1
print(ans) # 输出最大边长
- 设定初始的搜索范围为
left = 1
,right = 100000
,表示我们假定边长x
可能的最大值为100000
。 - 使用二分查找逐步缩小搜索范围来找到能切出至少
K
块小巧克力的最大边长x
。
【执行二分查找过程】
初始区间 [left=1, right=100000]
第一次循环:
- 计算中点:
mid = (1 + 100000) // 2 = 50000
。 - 调用
check(50000)
:- 对于第一块巧克力
(6, 5)
,50000
边长无法切割出任何小块(6 // 50000 = 0
,5 // 50000 = 0
),所以结果为0
块。 - 对于第二块巧克力
(5, 6)
,同样,50000
边长无法切割出任何小块(5 // 50000 = 0
,6 // 50000 = 0
),所以结果为0
块。 - 总共切出的块数为
0
,check(50000)
返回False
,因此调整搜索区间为[1, 49999]
。
- 对于第一块巧克力
第二次循环:
计算中点:
mid = (1 + 49999) // 2 = 25000
。调用
check(25000)
:- 对于第一块巧克力
(6, 5)
,25000
边长无法切割出任何小块(6 // 25000 = 0
,5 // 25000 = 0
),所以结果为0
块。 - 对于第二块巧克力
(5, 6)
,同样,25000
边长无法切割出任何小块(5 // 25000 = 0
,6 // 25000 = 0
),所以结果为0
块。 - 总共切出的块数为
0
,check(25000)
返回False
,因此调整搜索区间为[1, 24999]
。
- 对于第一块巧克力
继续减小搜索区间:直到找到一个合适的边长。
终止条件与最终结果
最终,通过多次二分查找,我们最终会找到 x = 2
是最大边长,能够切出至少 K = 10
块小巧克力。具体的结果是:
- 对于
x = 2
,第一块巧克力(6, 5)
可以切出6 // 2 = 3
行,5 // 2 = 2
列,总共能切出3 * 2 = 6
块。 - 对于
x = 2
,第二块巧克力(5, 6)
可以切出5 // 2 = 2
行,6 // 2 = 3
列,总共能切出2 * 3 = 6
块。 - 总共切出的块数是
6 + 6 = 12
块,满足K = 10
,因此check(2)
返回True
,最终输出2
。
④最终输出
2
【运行结果】