【蓝桥杯7天速成】第二十三章:Python二分查找与插入完全指南

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
  1. 前置条件验证
if not nums or nums[-1] < x:
    nums.append(x)
else:
    bisect.insort(nums, x)
  1. 性能优化
  • 对10万元素列表插入:普通插入需10万次比较,二分插入只需17次
  1. 应用场景推荐
  • 实时维护排行榜
  • 日志时间戳插入
  • 游戏高分记录
  • 数据库索引维护

重要提醒:bisect模块要求输入列表必须是有序的!

# 错误用法示例
unsorted = [3, 1, 4, 1, 5]
bisect.insort(unsorted, 2)  # 结果不可预测!

六、内部实现原理

二分查找算法流程

  1. 初始化左右指针(lo=0, hi=len(nums))
  2. 计算中间位置 mid = (lo + hi) // 2
  3. 比较中间元素与目标值
  4. 调整搜索范围(lo/hi = mid ±1)
  5. 重复直到找到插入位置

七、时间复杂度分析

  • 最优: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模块的使用,您可以高效处理各种有序数据操作需求!🎯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

monday_CN

72小时打磨,值得1元认可

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值