Skip to content

11.1 顺序查找与哨兵优化

顺序查找是最朴素的查找方法,就是从头到尾一个一个比对。虽然效率不高(时间复杂度O(n)),但胜在简单直接,不需要数据有序。

不过有个小技巧叫"哨兵优化",能减少每次循环的边界检查。原理是在数组末尾临时放一个要找的目标值,这样循环时就不用每次都判断是否越界了。

下面看看具体怎么实现:

python
def sequential_search_with_sentinel(arr, target):
    """
    使用哨兵优化的顺序查找
    参数:
        arr: 待查找的列表
        target: 目标值
    返回:
        找到返回索引,没找到返回-1
    """
    # 保存原数组长度和最后一个元素
    n = len(arr)
    if n == 0:
        return -1
    
    last_element = arr[-1]  # 保存最后一个元素
    arr[-1] = target        # 设置哨兵
    
    i = 0
    # 循环直到找到目标值(因为有哨兵,一定能在数组内找到)
    while arr[i] != target:
        i += 1
    
    # 恢复最后一个元素
    arr[-1] = last_element
    
    # 如果在最后一个位置找到,需要确认是不是真正的目标
    if i < n - 1 or last_element == target:
        return i
    else:
        return -1

# 测试代码
try:
    test_arr = [3, 7, 2, 9, 1, 5]
    result = sequential_search_with_sentinel(test_arr.copy(), 9)
    print(f"查找9的结果: {result}")  # 应该输出3
    
    result = sequential_search_with_sentinel(test_arr.copy(), 8)
    print(f"查找8的结果: {result}")  # 应该输出-1
    
except Exception as e:
    print(f"查找过程中出现错误: {e}")

顺序查找实例方法表

功能名称实例调用方法具体功能、注意事项、参数说明
基础顺序查找for i in range(len(arr)): if arr[i] == target: return i最简单的实现,每次循环都要检查边界,时间复杂度O(n),空间复杂度O(1)
哨兵优化查找sequential_search_with_sentinel(arr, target)减少边界检查次数,但需要修改原数组(所以通常传入副本),适用于频繁查找的场景

顺序查找虽然简单,但在某些特定场景下还是很有用的,比如数据量小或者数据无序的时候。哨兵优化这个小技巧体现了算法设计中"用空间换时间"的思想。

11.2 二分查找:循环不变式与边界控制

二分查找是查找算法中的明星选手,前提是数据必须有序。它的核心思想就是"猜中间,砍一半",时间复杂度只有O(log n),效率杠杠的。

不过二分查找看似简单,实际写起来很容易在边界条件上翻车。关键是要维护好"循环不变式"——也就是在整个循环过程中始终保持的某种状态。

最常见的有两种写法:左闭右闭区间[left, right]和左闭右开区间[left, right)。我们来看看左闭右闭的实现:

python
def binary_search(arr, target):
    """
    标准二分查找实现(左闭右闭区间)
    参数:
        arr: 已排序的列表(升序)
        target: 目标值
    返回:
        找到返回索引,没找到返回-1
    """
    left, right = 0, len(arr) - 1  # 初始化左右边界
    
    # 循环条件:left <= right,因为区间是左闭右闭
    while left <= right:
        mid = left + (right - left) // 2  # 防止整数溢出的写法
        
        if arr[mid] == target:
            return mid  # 找到了,返回索引
        elif arr[mid] < target:
            left = mid + 1  # 目标在右半部分
        else:
            right = mid - 1  # 目标在左半部分
    
    return -1  # 没找到

# 测试代码
try:
    sorted_arr = [1, 3, 5, 7, 9, 11, 13, 15]
    result = binary_search(sorted_arr, 7)
    print(f"查找7的结果: {result}")  # 应该输出3
    
    result = binary_search(sorted_arr, 8)
    print(f"查找8的结果: {result}")  # 应该输出-1
    
except Exception as e:
    print(f"二分查找过程中出现错误: {e}")

二分查找实例方法表

功能名称实例调用方法具体功能、注意事项、参数说明
标准二分查找binary_search(arr, target)要求输入数组已排序,返回第一个匹配的索引,时间复杂度O(log n),空间复杂度O(1)
查找插入位置bisect.bisect_left(arr, target)Python内置模块,返回target应该插入的位置以保持有序

二分查找的关键在于理解循环不变式的含义:在每次循环开始时,如果目标存在,那么它一定在当前的搜索区间内。掌握这一点,边界条件就不会搞错了。

11.3 二分答案:在解空间中搜索

二分查找不仅能用来找数组中的元素,还能用来解决更复杂的问题——"二分答案"。这种思路特别巧妙:当我们要求解某个最优值时,可以先猜测一个答案,然后验证这个答案是否可行。

比如经典的"爱吃香蕉的珂珂"问题:给定若干堆香蕉和小时数H,求珂珂每小时最少吃多少根才能在H小时内吃完。这个问题的答案具有单调性——如果每小时吃K根能完成,那么吃K+1根也一定能完成。

python
def min_eating_speed(piles, h):
    """
    二分答案:求最小吃香蕉速度
    参数:
        piles: 香蕉堆列表,piles[i]表示第i堆的香蕉数量
        h: 可用小时数
    返回:
        最小吃香蕉速度(每小时根数)
    """
    def can_finish(speed):
        """验证给定速度是否能在h小时内吃完"""
        hours_needed = 0
        for pile in piles:
            # 向上取整:(pile + speed - 1) // speed
            hours_needed += (pile + speed - 1) // speed
        return hours_needed <= h
    
    # 答案的下界是1,上界是最大堆的香蕉数
    left, right = 1, max(piles)
    
    while left < right:
        mid = left + (right - left) // 2
        if can_finish(mid):
            right = mid  # 当前速度可行,尝试更小的速度
        else:
            left = mid + 1  # 当前速度不可行,需要更大的速度
    
    return left

# 测试代码
try:
    piles = [3, 6, 7, 11]
    h = 8
    result = min_eating_speed(piles, h)
    print(f"最小吃香蕉速度: {result}")  # 应该输出4
    
    piles = [30, 11, 23, 4, 20]
    h = 5
    result = min_eating_speed(piles, h)
    print(f"最小吃香蕉速度: {result}")  # 应该输出30
    
except Exception as e:
    print(f"二分答案过程中出现错误: {e}")

二分答案实例方法表

功能名称实例调用方法具体功能、注意事项、参数说明
最小化最大值min_eating_speed(piles, h)适用于"最小化最大消耗/时间/成本"类问题,需要定义验证函数
最大化最小值类似框架,调整验证逻辑适用于"最大化最小收益/距离/容量"类问题,同样需要验证函数

二分答案的核心是发现答案的单调性:如果某个答案可行,那么所有"更好"的答案都可行(或都不可行)。这种思路把优化问题转化为了判定问题,大大简化了思考过程。

11.4 旋转有序数组中的查找

旋转有序数组是个有趣的变种:原本有序的数组被从某个位置切开,前后两部分交换位置。比如[0,1,2,4,5,6,7]可能变成[4,5,6,7,0,1,2]

在这种数组中查找目标值,不能直接用标准二分查找,但可以利用数组的部分有序性。关键观察是:无论怎么旋转,数组总有一半是有序的。

python
def search_rotated_array(nums, target):
    """
    在旋转有序数组中查找目标值
    参数:
        nums: 旋转后的有序数组(无重复元素)
        target: 目标值
    返回:
        找到返回索引,没找到返回-1
    """
    left, right = 0, len(nums) - 1
    
    while left <= right:
        mid = left + (right - left) // 2
        
        if nums[mid] == target:
            return mid
        
        # 判断左半部分是否有序
        if nums[left] <= nums[mid]:
            # 左半部分有序
            if nums[left] <= target < nums[mid]:
                # 目标在左半部分
                right = mid - 1
            else:
                # 目标在右半部分
                left = mid + 1
        else:
            # 右半部分有序
            if nums[mid] < target <= nums[right]:
                # 目标在右半部分
                left = mid + 1
            else:
                # 目标在左半部分
                right = mid - 1
    
    return -1

# 测试代码
try:
    rotated_arr = [4, 5, 6, 7, 0, 1, 2]
    result = search_rotated_array(rotated_arr, 0)
    print(f"查找0的结果: {result}")  # 应该输出4
    
    result = search_rotated_array(rotated_arr, 3)
    print(f"查找3的结果: {result}")  # 应该输出-1
    
    # 边界情况:未旋转的数组
    normal_arr = [0, 1, 2, 4, 5, 6, 7]
    result = search_rotated_array(normal_arr, 4)
    print(f"查找4的结果: {result}")  # 应该输出3
    
except Exception as e:
    print(f"旋转数组查找过程中出现错误: {e}")

旋转数组查找实例方法表

功能名称实例调用方法具体功能、注意事项、参数说明
无重复旋转数组查找search_rotated_array(nums, target)数组中无重复元素,时间复杂度O(log n),需要判断哪一半有序
有重复旋转数组查找需要额外处理nums[left] == nums[mid]的情况当存在重复元素时,最坏情况退化为O(n),需要跳过重复元素

旋转有序数组的查找展示了二分查找的灵活性:即使数组不是完全有序的,只要存在某种规律性,就可能用二分的思想来解决。关键是要找到可以利用的有序部分。