Python 算法基础篇之查找算法(一):顺序查找、二分查找与插值查找

1. 查找算法概述:为什么需要不同的查找方式?

1.1 什么是查找?

查找(Search):在数据集合中寻找满足特定条件的元素的过程。

查找是计算机科学中最基础、最高频的操作之一:

  • 数据库查询:SELECT * FROM users WHERE id = 10086
  • 搜索引擎:输入关键词,返回相关网页
  • 代码补全:IDE根据前缀查找匹配的方法名
  • 游戏匹配:根据分数查找水平相近的对手

1.2 查找算法的分类

复制代码
查找算法分类:
  静态查找:数据集合不变                    
   ├── 顺序查找(线性查找)                  
   ├── 二分查找(折半查找)                  
   ├── 插值查找                             
   ├── 斐波那契查找 ← 第二篇                 
   └── 分块查找 ← 第二篇                    
  动态查找:数据集合会变化                  
   ├── 二叉搜索树 ← 第三篇                   
   ├── 平衡二叉树(AVL)← 第三篇             
   ├── B树/B+树 ← 第三篇                    
   └── 哈希查找 ← 第二篇                   

1.3 评价查找算法的标准

指标 说明 重要性
时间复杂度 查找所需时间与数据规模的关系 ⭐⭐⭐
空间复杂度 额外内存占用 ⭐⭐⭐
适用条件 对数据的要求(有序/无序、静态/动态) ⭐⭐⭐
平均查找长度(ASL) 查找成功时平均比较次数 ⭐⭐

平均查找长度(ASL) :ASL=∑i=1nPi×CiASL = \sum_{i=1}^{n} P_i \times C_iASL=∑i=1nPi×Ci,其中 PiP_iPi 是第 iii 个元素被查找的概率,CiC_iCi 是找到该元素所需的比较次数。


2. 顺序查找:最简单的暴力美学

2.1 算法思想

顺序查找(Sequential Search):从数据集合的一端开始,逐个元素与目标值比较,直到找到或遍历完所有元素。

生活类比:在杂乱的抽屉里找钥匙,只能一个一个翻。

复制代码
查找过程可视化:

数据:[34, 12, 56, 78, 9, 23, 67]
目标:23

第1次比较:34 == 23? ❌ 不是
            ↑
第2次比较:12 == 23? ❌ 不是
               ↑
第3次比较:56 == 23? ❌ 不是
                  ↑
第4次比较:78 == 23? ❌ 不是
                     ↑
第5次比较:9 == 23?  ❌ 不是
                        ↑
第6次比较:23 == 23? ✅ 找到!索引=5
                           ↑

共比较6次

2.2 基础实现

python 复制代码
def sequential_search(arr: list, target) -> int:
    """
    顺序查找基础版
    
    参数:
        arr: 待查找的列表(无序)
        target: 目标值
    
    返回:
        目标值的索引,未找到返回 -1
    
    时间复杂度:O(n)
    空间复杂度:O(1)
    """
    for i in range(len(arr)):
        if arr[i] == target:
            return i  # 找到,返回索引
    
    return -1  # 未找到


# 测试
data = [34, 12, 56, 78, 9, 23, 67]
print(sequential_search(data, 23))   # 5
print(sequential_search(data, 100))  # -1

2.3 优化:哨兵顺序查找

哨兵(Sentinel)技巧:将目标值放到数组末尾作为"哨兵",省去每次循环的边界检查。

python 复制代码
def sequential_search_sentinel(arr: list, target) -> int:
    """
    带哨兵的顺序查找
    
    优化点:省去每次循环的 i < n 判断
    """
    n = len(arr)
    if n == 0:
        return -1
    
    # 保存最后一个元素
    last = arr[-1]
    
    # 设置哨兵
    arr[-1] = target
    
    i = 0
    # 无需判断 i < n,因为一定能找到(至少找到哨兵)
    while arr[i] != target:
        i += 1
    
    # 恢复原始数据
    arr[-1] = last
    
    # 判断是真的找到还是找到哨兵
    if i < n - 1 or last == target:
        return i
    
    return -1


# 测试
data = [34, 12, 56, 78, 9, 23, 67]
print(sequential_search_sentinel(data, 23))   # 5
print(sequential_search_sentinel(data, 67))   # 6(最后一个元素)
print(sequential_search_sentinel(data, 100))  # -1

性能对比:

版本 每次循环操作 优势
基础版 比较 arr[i] == target + 判断 i < len(arr) 代码直观
哨兵版 仅比较 arr[i] == target 减少约30%的比较次数

2.4 顺序查找的适用场景

python 复制代码
# ✅ 适用场景

# 1. 数据量小(n < 100)
small_data = [5, 2, 8, 1]
print(sequential_search(small_data, 8))  # 2

# 2. 数据无序且无法排序
unsorted_records = [
    {'id': 102, 'name': 'Alice'},
    {'id': 58, 'name': 'Bob'},
    {'id': 231, 'name': 'Charlie'}
]
target_id = 58
for i, record in enumerate(unsorted_records):
    if record['id'] == target_id:
        print(f"找到:{record}")  # 找到:{'id': 58, 'name': 'Bob'}
        break

# 3. 链表结构(无法随机访问)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def search_linked_list(head: ListNode, target) -> bool:
    """链表只能顺序查找"""
    current = head
    while current:
        if current.val == target:
            return True
        current = current.next
    return False

3. 二分查找:折半的艺术

3.1 算法思想

二分查找(Binary Search) :在有序数组中,每次将查找范围缩小一半,通过比较中间元素与目标值,决定向左还是向右继续查找。

生活类比:查字典时,先看中间的页,决定往前翻还是往后翻。

复制代码
查找过程可视化:

数据(已排序):[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
目标:23

初始范围:left=0, right=9

第1轮:
    mid = (0 + 9) // 2 = 4
    arr[4] = 16
    16 < 23?是,目标在右半区
    left = mid + 1 = 5
    
    [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
                        ↑left        ↑right
                        └─ 新范围 ──┘

第2轮:
    mid = (5 + 9) // 2 = 7
    arr[7] = 56
    56 > 23?是,目标在左半区
    right = mid - 1 = 6
    
    [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
                        ↑left  ↑right
                        └范围┘

第3轮:
    mid = (5 + 6) // 2 = 5
    arr[5] = 23
    23 == 23?✅ 找到!索引=5
    
    共比较3次(顺序查找需要6次)

3.2 基础实现:迭代版

python 复制代码
def binary_search(arr: list, target) -> int:
    """
    二分查找:迭代版
    
    前提:arr 必须是有序的(升序)
    
    参数:
        arr: 已排序的列表
        target: 目标值
    
    返回:
        目标值的索引,未找到返回 -1
    
    时间复杂度:O(log n)
    空间复杂度:O(1)
    """
    left, right = 0, len(arr) - 1
    
    while left <= right:
        # 防止溢出的写法:(left + right) // 2 的等价形式
        mid = left + (right - left) // 2
        
        if arr[mid] == target:
            return mid  # 找到
        elif arr[mid] < target:
            left = mid + 1  # 目标在右半区
        else:
            right = mid - 1  # 目标在左半区
    
    return -1  # 未找到


# 测试
sorted_data = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(sorted_data, 23))   # 5
print(binary_search(sorted_data, 2))    # 0(第一个元素)
print(binary_search(sorted_data, 91))   # 9(最后一个元素)
print(binary_search(sorted_data, 100))  # -1(不存在)

⚠️ 关键细节mid = left + (right - left) // 2 而不是 (left + right) // 2,防止整数溢出(虽然Python整数不会溢出,但这是良好的编程习惯)。

3.3 递归实现

python 复制代码
def binary_search_recursive(arr: list, target, left=0, right=None) -> int:
    """
    二分查找:递归版
    
    时间复杂度:O(log n)
    空间复杂度:O(log n) - 递归栈空间
    """
    if right is None:
        right = len(arr) - 1
    
    # 递归终止条件
    if left > right:
        return -1
    
    mid = left + (right - left) // 2
    
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search_recursive(arr, target, mid + 1, right)
    else:
        return binary_search_recursive(arr, target, left, mid - 1)


# 测试
print(binary_search_recursive(sorted_data, 23))   # 5
print(binary_search_recursive(sorted_data, 100))  # -1

3.4 查找边界:找第一个/最后一个等于目标的位置

python 复制代码
def binary_search_first(arr: list, target) -> int:
    """
    查找第一个等于 target 的位置(可能有重复元素)
    
    关键:找到 target 后不立即返回,继续向左查找
    """
    left, right = 0, len(arr) - 1
    result = -1
    
    while left <= right:
        mid = left + (right - left) // 2
        
        if arr[mid] == target:
            result = mid      # 记录位置
            right = mid - 1   # 继续向左查找
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return result


def binary_search_last(arr: list, target) -> int:
    """
    查找最后一个等于 target 的位置
    """
    left, right = 0, len(arr) - 1
    result = -1
    
    while left <= right:
        mid = left + (right - left) // 2
        
        if arr[mid] == target:
            result = mid      # 记录位置
            left = mid + 1    # 继续向右查找
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return result


# 测试重复数据
data_with_dup = [1, 2, 4, 4, 4, 5, 6, 7]
print(binary_search_first(data_with_dup, 4))   # 2(第一个4)
print(binary_search_last(data_with_dup, 4))    # 4(最后一个4)

3.5 查找插入位置

python 复制代码
def binary_search_insert_position(arr: list, target) -> int:
    """
    查找 target 应该插入的位置(保持有序)
    
    返回:target 应该插入的索引
    """
    left, right = 0, len(arr) - 1
    
    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
    
    # left 就是应该插入的位置
    return left


# 测试
sorted_arr = [1, 3, 5, 7, 9]
print(binary_search_insert_position(sorted_arr, 6))   # 3(插入到5和7之间)
print(binary_search_insert_position(sorted_arr, 0))   # 0(插入到开头)
print(binary_search_insert_position(sorted_arr, 10))  # 5(插入到末尾)

3.7 二分查找的适用场景与陷阱

python 复制代码
# ✅ 适用场景

# 1. 静态有序数据
sorted_ids = [1001, 1005, 1008, 1012, 1015, 1020, 1025]
print(binary_search(sorted_ids, 1015))  # 4

# 2. 答案具有单调性的问题(二分答案)
def can_finish(piles, speed, h):
    """能否在h小时内以speed速度吃完所有香蕉"""
    hours = sum((p + speed - 1) // speed for p in piles)
    return hours <= h

def min_eating_speed(piles, h):
    """
    Koko吃香蕉:求最小速度
    二分查找答案范围
    """
    left, right = 1, max(piles)
    
    while left < right:
        mid = left + (right - left) // 2
        if can_finish(piles, mid, h):
            right = mid  # 可以完成,尝试更慢
        else:
            left = mid + 1  # 无法完成,需要更快
    
    return left

print(min_eating_speed([3, 6, 7, 11], 8))  # 4


# ❌ 常见陷阱

# 陷阱1:数据未排序!
unsorted = [5, 2, 8, 1, 9]
# binary_search(unsorted, 8)  # 可能返回错误结果!

# 陷阱2:整数溢出(其他语言)
# mid = (left + right) // 2  # Java/C++中可能溢出!
# 正确:mid = left + (right - left) // 2

# 陷阱3:循环条件写错
# while left < right:  # 可能漏掉元素
# while left <= right:  # 正确

4. 插值查找:二分查找的智能升级

4.1 算法思想

插值查找(Interpolation Search) :二分查找的改进版。不是简单地取中间位置,而是根据目标值与当前范围最小/最大值的比例,智能预测目标值的位置。

核心公式

mid=left+target−arr[left]arr[right]−arr[left]×(right−left)mid = left + \frac{target - arr[left]}{arr[right] - arr[left]} \times (right - left)mid=left+arr[right]−arr[left]target−arr[left]×(right−left)

生活类比:查电话号码簿找"张三",不会从中间翻,而是直接从前面翻(因为"张"在拼音中靠前)。

复制代码
插值查找 vs 二分查找:

数据:[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
目标:10

二分查找:
    mid = (0 + 9) // 2 = 4,arr[4] = 50
    50 > 10,向左
    mid = (0 + 3) // 2 = 1,arr[1] = 20
    20 > 10,向左
    mid = (0 + 0) // 2 = 0,arr[0] = 10
    找到!比较3次

插值查找:
    mid = 0 + (10-10)/(100-10) * (9-0) = 0
    arr[0] = 10
    找到!比较1次!

4.2 代码实现

python 复制代码
def interpolation_search(arr: list, target) -> int:
    """
    插值查找
    
    前提:
        1. arr 必须是有序的
        2. 数据最好是均匀分布的(这是插值查找的优势场景)
    
    参数:
        arr: 已排序列表
        target: 目标值
    
    返回:
        目标值的索引,未找到返回 -1
    
    时间复杂度:
        最好:O(log log n) - 数据均匀分布
        平均:O(log log n)
        最坏:O(n) - 数据极度不均匀
    空间复杂度:O(1)
    """
    left, right = 0, len(arr) - 1
    
    while left <= right:
        # 边界检查
        if arr[left] == arr[right]:
            if arr[left] == target:
                return left
            break
        
        # 目标值不在范围内
        if target < arr[left] or target > arr[right]:
            break
        
        # 插值公式计算 mid
        # mid = left + (target - arr[left]) / (arr[right] - arr[left]) * (right - left)
        mid = left + int(
            (target - arr[left]) / (arr[right] - arr[left]) * (right - left)
        )
        
        # 防止计算错误导致越界
        mid = max(left, min(right, mid))
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1


# 测试:均匀分布数据
uniform_data = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
print(interpolation_search(uniform_data, 10))   # 0(1次找到)
print(interpolation_search(uniform_data, 100))  # 9(1次找到)
print(interpolation_search(uniform_data, 55))   # 5(约1-2次找到)

# 测试:非均匀分布数据
non_uniform = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1000]
print(interpolation_search(non_uniform, 1000))  # 9
print(interpolation_search(non_uniform, 5))     # 4

4.3 插值查找的详细过程可视化

复制代码
数据:[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
目标:75

初始:left=0(arr[0]=10), right=9(arr[9]=100)

第1轮:
    mid = 0 + (75-10)/(100-10) * (9-0)
        = 0 + 65/90 * 9
        = 0 + 6.5
        ≈ 6
    
    arr[6] = 70
    70 < 75?是,目标在右半区
    left = 7

第2轮:
    left=7(arr[7]=80), right=9(arr[9]=100)
    
    mid = 7 + (75-80)/(100-80) * (9-7)
        = 7 + (-5)/20 * 2
        = 7 - 0.5
        ≈ 7
    
    arr[7] = 80
    80 > 75?是,目标在左半区
    right = 6

等等!left=7 > right=6,循环结束?
实际上需要处理:当计算出的mid等于left且arr[mid]<target时,
left = mid + 1 可能导致 left > right

修正后的查找:
第2轮(重新计算):
    由于 arr[7]=80 > 75,且 arr[6]=70 < 75
    如果严格 left <= right:
    left=7, right=9
    mid = 7 + (75-80)/(100-80) * 2 = 6(取整后)
    但 mid 必须 >= left,所以 mid = 7
    arr[7]=80 > 75,right = 6
    现在 left=7 > right=6,退出循环,返回 -1

等等,75 不在数组中!正确结果是 -1

如果目标是 70:
第1轮:mid = 0 + (70-10)/(100-10) * 9 = 6
       arr[6] = 70,找到!仅1次比较!

4.4 改进版:处理重复和边界

python 复制代码
def interpolation_search_v2(arr: list, target) -> int:
    """
    改进版插值查找:更健壮的边界处理
    """
    left, right = 0, len(arr) - 1
    
    while left <= right:
        # 处理边界情况
        if left == right:
            return left if arr[left] == target else -1
        
        # 确保不会除零,且数据在范围内
        if arr[left] == arr[right]:
            if arr[left] == target:
                return left
            break
        
        if target < arr[left] or target > arr[right]:
            break
        
        # 插值计算,使用浮点数提高精度
        ratio = (target - arr[left]) / (arr[right] - arr[left])
        mid = left + int(ratio * (right - left))
        
        # 双重保险
        mid = max(left, min(right, mid))
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1

4.5 插值查找 vs 二分查找:何时用哪个?

场景 推荐算法 原因
数据均匀分布(如等差数列) 插值查找 mid计算准确,接近O(log log n)
数据不均匀分布(如指数增长) 二分查找 插值预测不准,退化到O(n)
数据分布未知 二分查找 稳定的O(log n),更可靠
数据量极小(n < 50) 顺序查找 常数因子小,更简单
数据量极大且均匀 插值查找 理论性能更优

5. 三种查找算法对比总结

5.1 核心指标对比

指标 顺序查找 二分查找 插值查找
前提条件 无(无序也可) 必须有序 必须有序+均匀分布
时间复杂度(最好) O(1) O(1) O(1)
时间复杂度(平均) O(n) O(log n) O(log log n)
时间复杂度(最坏) O(n) O(log n) O(n)
空间复杂度 O(1) O(1) O(1)
数据适应性 任意数据 有序数据 均匀分布数据
实现复杂度 简单 中等 较复杂

5.2 决策流程图

复制代码
开始查找
  │
  ▼
数据是否有序? ──否──→ 数据量小(n<100)? ──是──→ 顺序查找
  │                      │
  是                     否
  │                      ▼
  ▼                   能否排序? ──是──→ 排序后用二分/插值
数据是否均匀分布?        │
  │                     否
  是                     ▼
  ▼                   顺序查找
插值查找
  │
  否
  ▼
二分查找

5.3 代码实现复杂度对比

python 复制代码
# 顺序查找:最简洁
def sequential_search(arr, target):
    for i, x in enumerate(arr):
        if x == target:
            return i
    return -1

# 二分查找:需注意边界
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    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

# 插值查找:最复杂,需处理边界
def interpolation_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right and arr[left] <= target <= arr[right]:
        if left == right:
            return left if arr[left] == target else -1
        mid = left + int((target - arr[left]) / (arr[right] - arr[left]) * (right - left))
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

6. 实战练习

6.1 练习一:旋转排序数组的查找

python 复制代码
def search_rotated_array(arr: list, target) -> int:
    """
    查找旋转排序数组中的目标值
    
    例:[4, 5, 6, 7, 0, 1, 2] 是 [0, 1, 2, 4, 5, 6, 7] 的旋转
    
    思路:修改二分查找,先判断哪一半是有序的
    """
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = left + (right - left) // 2
        
        if arr[mid] == target:
            return mid
        
        # 左半部分有序
        if arr[left] <= arr[mid]:
            if arr[left] <= target < arr[mid]:
                right = mid - 1
            else:
                left = mid + 1
        # 右半部分有序
        else:
            if arr[mid] < target <= arr[right]:
                left = mid + 1
            else:
                right = mid - 1
    
    return -1


# 测试
rotated = [4, 5, 6, 7, 0, 1, 2]
print(search_rotated_array(rotated, 0))   # 4
print(search_rotated_array(rotated, 3))   # -1

6.2 练习二:查找峰值元素

python 复制代码
def find_peak_element(arr: list) -> int:
    """
    查找峰值元素(比相邻元素大的元素)
    
    假设:arr[-1] = arr[n] = -∞
    
    思路:二分查找的变形,比较 mid 与 mid+1
    """
    left, right = 0, len(arr) - 1
    
    while left < right:
        mid = left + (right - left) // 2
        
        if arr[mid] > arr[mid + 1]:
            # 峰值在左半区(包含mid)
            right = mid
        else:
            # 峰值在右半区
            left = mid + 1
    
    return left  # 或 right,此时 left == right


# 测试
print(find_peak_element([1, 2, 3, 1]))       # 2(峰值3)
print(find_peak_element([1, 2, 1, 3, 5, 6, 4]))  # 5(峰值6)

6.3 练习三:平方根的二分查找

python 复制代码
def my_sqrt(x: int) -> int:
    """
    计算平方根的整数部分(不使用内置函数)
    
    思路:二分查找答案
    """
    if x < 2:
        return x
    
    left, right = 1, x // 2
    
    while left <= right:
        mid = left + (right - left) // 2
        square = mid * mid
        
        if square == x:
            return mid
        elif square < x:
            left = mid + 1
        else:
            right = mid - 1
    
    # right 是最后一个满足 right² <= x 的数
    return right


# 测试
print(my_sqrt(4))    # 2
print(my_sqrt(8))    # 2(√8 ≈ 2.828,取整2)
print(my_sqrt(16))   # 4
print(my_sqrt(2147395599))  # 46339
相关推荐
阿文的代码库1 小时前
对于C++中push_back的原理介绍与分析
开发语言·c++
2401_867623981 小时前
如何设置用户默认表空间_ALTER USER DEFAULT TABLESPACE
jvm·数据库·python
ftpeak1 小时前
LangGraph Agent 开发指南(12~函数式 API)
人工智能·python·ai·langchain·langgraph
ChoSeitaku1 小时前
06_可变参数_递归_类和对象_封装
java·数据结构·算法
枕星而眠1 小时前
C++ 核心语法精讲:auto / 模板 / 命名空间 / 动态内存 从用法到面试
开发语言·c++·面试
yivifu1 小时前
跟水印杠上了——顺便巩固Tkinter的GUI编程
python·opencv·tkinter·去水印
2301_803934611 小时前
html标签怎样划分页面区域_section与div的区别【介绍】
jvm·数据库·python
-To be number.wan1 小时前
算法日记 | 动态规划(初级)
算法·动态规划
_深海凉_1 小时前
LeetCode热题100-二叉搜索树中第 K 小的元素
算法·leetcode·职场和发展