Python二分查找与插入完全指南
二分法定义:把一个长度为n的有序序列上O(n)的查找时间,优化到了O(logn) 。
二分法本质:折半搜索。
二分法效率:很高,时间复杂度为 O(logn) (其实不太严谨,因为需要考虑底数,那就要看分治的复杂度:二分法底数为 2,则为复杂度为 O(log2n) ;三分法底数为 3,则为 O(log3n) … 以此类推。当然不写底数也行,但是得知道它有底数)。
二分法实例——猜数游戏:若n=1000 万,只需要猜 log2107=24 次
一、模块导入与核心函数
import bisect
# 主要函数清单
bisect.bisect_left() # 查找左侧插入点
bisect.bisect_right() # 查找右侧插入点
bisect.insort_left() # 左侧插入保持有序
bisect.insort_right() # 右侧插入保持有序
🔍 二分查找详解
1. bisect_left() 查找左侧插入点
nums = [1, 2, 4, 5, 7, 9]
pos = bisect.bisect_left(nums, 3)
print(pos) # 输出:2(插入到元素4前)
2. bisect_right() 查找右侧插入点
pos = bisect.bisect_right(nums, 5)
print(pos) # 输出:4(插入到元素7前)
查找逻辑图示
原数组:[1, 2, 4, 5, 7, 9]
查找3 → 插入到索引2的位置
查找5 → bisect_left返回3,bisect_right返回4
三、有序插入操作
1. insort_left() 左侧插入
bisect.insort_left(nums, 3)
print(nums) # [1, 2, 3, 4, 5, 7, 9]
2. insort_right() 右侧插入
bisect.insort_right(nums, 5)
print(nums) # [1, 2, 3, 4, 5, 5, 7, 9]
插入性能对比
操作方式 | 时间复杂度 | 适用场景 |
---|---|---|
普通插入 | O(n) | 小型数据集 |
二分插入 | O(log n) | 大型有序数据集 |
四、高级使用技巧
1. 自定义键值查找
class Student:
def __init__(self, score):
self.score = score
students = [Student(60), Student(75), Student(90)]
scores = [s.score for s in students]
# 查找80分应插入的位置
pos = bisect.bisect_left(scores, 80)
print(f"应插入到索引{pos}位置") # 输出:2
2. 处理重复元素
nums = [1, 2, 2, 2, 3]
# 查找第一个2的位置
first = bisect.bisect_left(nums, 2) # 1
# 查找最后一个2的后一个位置
last = bisect.bisect_right(nums, 2) # 4
3. 指定查找范围
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
lo, hi = 3, 7 # 仅在[3,7)区间查找
pos = bisect.bisect_left(nums, 5, lo, hi) # 返回5
假设我们有一个已排序的数组,并想在其中查找一个元素:
arr = [2, 3, 4, 10, 40]
x = 10
# 使用递归方法查找
result_recursive = binary_search_recursive(arr, 0, len(arr) - 1, x)
print(f"Element found at index {result_recursive} using recursive method.")
# 使用循环方法查找
result_iterative = binary_search_iterative(arr, x)
print(f"Element found at index {result_iterative} using iterative method.")
五、最佳实践建议
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
示例 :
输入:nums = [-1,0,3,5,9,12], target = 9
输出:4
解释:9 出现在 nums 中并且下标为 4
示例 2:
输入:nums = [-1,0,3,5,9,12], target = 2
输出:-1
解释:2 不存在 nums 中因此返回 -1
题解
class BinarySearch(object):
def binary_search(self, nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (right - left) // 2 + left
# mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
return -1
# search = BinarySearch()
# nums = [-1, 0, 3, 5, 9, 12]
# target = 9
# print(search.binary_search(nums, target)) # 输出:4
- 前置条件验证
if not nums or nums[-1] < x:
nums.append(x)
else:
bisect.insort(nums, x)
- 性能优化
- 对10万元素列表插入:普通插入需10万次比较,二分插入只需17次
- 应用场景推荐
- 实时维护排行榜
- 日志时间戳插入
- 游戏高分记录
- 数据库索引维护
重要提醒:bisect模块要求输入列表必须是有序的!
# 错误用法示例
unsorted = [3, 1, 4, 1, 5]
bisect.insort(unsorted, 2) # 结果不可预测!
六、内部实现原理
二分查找算法流程
- 初始化左右指针(lo=0, hi=len(nums))
- 计算中间位置 mid = (lo + hi) // 2
- 比较中间元素与目标值
- 调整搜索范围(lo/hi = mid ±1)
- 重复直到找到插入位置
七、时间复杂度分析
- 最优:O(1)(直接命中中间元素)
- 平均:O(log n)
- 最差:O(log n)
- 时间复杂度分析
无论是递归实现还是迭代实现,二分查找的时间复杂度都是O(log n)。这里的“log”是以2为底的对数。这是因为每次查找后,搜索范围大约减半。例如:
如果数组长度为8(2^3),最多需要3次比较。
如果数组长度为16(2^4),最多需要4次比较。
对于长度为n的数组,最多需要log2(n)次比较。
- 空间复杂度分析(对于递归版本)
递归实现的二分查找在空间复杂度上稍微复杂一些,因为它依赖于系统栈的深度来保存每次递归调用的状态。在最坏的情况下(即每次查找后都选择右半部分),递归深度可以达到log n。因此,递归实现的空间复杂度也是O(log n)。然而,在实践中,Python的实现通常会优化尾递归,使得空间复杂度接近于迭代的版本,即O(1)。但对于初学者而言,理解其为O(log n)是有帮助的。
1. 递归实现
def binary_search_recursive(arr, low, high, x):
# 检查base case
if high >= low:
mid = (high + low) // 2
# 如果元素在中间,则返回其索引
if arr[mid] == x:
return mid
# 如果元素小于中间元素,则在左侧子数组中查找
elif arr[mid] > x:
return binary_search_recursive(arr, low, mid - 1, x)
# 如果元素大于中间元素,则在右侧子数组中查找
else:
return binary_search_recursive(arr, mid + 1, high, x)
else:
# 元素不在数组中
return -1
2. 迭代实现
迭代的二分查找算法与递归版本类似,但它使用循环而不是递归调用。
def binary_search_iterative(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1 # 目标值不存在
3. 循环实现
def binary_search_iterative(arr, x):
low = 0
high = len(arr) - 1
while low <= high:
mid = (high + low) // 2
# 如果元素在中间,则返回其索引
if arr[mid] == x:
return mid
# 如果元素小于中间元素,则在左侧子数组中查找
elif arr[mid] > x:
high = mid - 1
# 如果元素大于中间元素,则在右侧子数组中查找
else:
low = mid + 1
# 元素不在数组中
return -1
注意点:
二分查找要求数组或列表是有序的。如果数组未排序,则应先进行排序。例如,使用sorted()函数或者arr.sort()方法。
在实际应用中,通常推荐使用迭代版本,因为其通常比递归版本更高效(特别是在某些Python解释器中,由于递归可能导致的栈溢出问题)。然而,递归版本在某些情况下更直观易懂。选择哪种实现方式取决于个人偏好和具体需求。
总结
虽然二分查找的实现方式(递归或迭代)在时间复杂度上是一样的,但在实际应用中,迭代版本通常更受推荐,因为它避免了递归可能带来的栈溢出风险,并且在某些情况下(如尾递归优化不完全的情况下)可能在理论上具有更优的空间复杂度表现。在实际的Python环境中,由于Python的尾递归优化,这两种方法在实践中差别不大。但在理论上和某些编程语言中(例如某些编译器不支持尾递归优化的语言),迭代方法更为稳妥。
通过掌握bisect模块的使用,您可以高效处理各种有序数据操作需求!🎯