⌨️ 第四章 流程图与算法
📄 一句话概括

找数据两条路:线性查找挨个问(O(n),但不挑食——乱的不乱的都行);二分查找每问一次砍一半(O(log n),但前提是已排序)——一百万数据前者最多一百万次、后者二十次,差距全在【数据有没有提前排好】。

📝 本节和前面几节的关系

4-14 的账本在这一篇兑现:O(n) 对 O(log n) 的正面交锋。回收 3-17 的二叉搜索树(每层砍一半的同构思想)与数据库课 7-7 索引(【先排序再查】的数据库形态)。

一、线性查找:挨个问

def linear_search(arr, target):
    for i, x in enumerate(arr):     # 从头到尾挨个比
        if x == target:
            return i                # 找到返回下标
    return -1                       # 没找到

print(linear_search([8, 3, 9, 1], 9))    # 2

性格:O(n) 最坏查完整表;不要求有序、链表也能用(顺序访问正合适,回收 3-15);数据一百万就要问一百万次——简单但贵。

二、二分查找:每次砍一半

前提:已排序。玩法像【猜 1~100 的数字】:报中间、听【大了/小了】、范围立刻砍半:

def binary_search(arr, target):     # arr 必须升序!
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2        # 中点
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1            # 目标在右半:左边界跳过中点
        else:
            hi = mid - 1            # 目标在左半:右边界退回中点前
    return -1

arr = [1, 3, 5, 7, 9, 11]
print(binary_search(arr, 7))        # 3

为什么是 log n:范围每轮除以 2,一百万个元素约 20 轮见分晓(log2(1000000)≈19.9,4-14 刚验过)。排序一次 O(n log n),之后成千上万次查找各 O(log n)——查找频繁时这笔投资稳赚(数据库索引的全部经济学)。

三、三分天下:查找方案选型

方案 查找 前提 适用
线性查找 O(n) 无 小数据、链表、就查一两次
二分查找 O(log n) 必须有序 静态数据、频繁查找
哈希(3-13) O(1) 内存换速度、无序 键值查询、超大表

注意二分不占地:有序数组好二分但插删要搬家(3-15);要【又频繁插删又快速查】就升级到平衡树(3-17)——三招是一族演进。

四、动手 Demo:步数现场对比

arr = list(range(0, 2_000_000, 2))     # 100 万个偶数(已有序)
target = 1_999_998

steps = 0
lo, hi = 0, len(arr) - 1
while lo <= hi:
    mid = (lo + hi) // 2
    steps += 1
    if arr[mid] == target:
        break
    elif arr[mid] < target:
        lo = mid + 1
    else:
        hi = mid - 1
print("二分命中,下标", mid, ",步数", steps)     # 步数 20
print("线性要", len(arr), "步")                  # 1000000

看到什么算成功:二分命中,下标 999999 ,步数 20、线性要 1000000 步。

读结果:20 对 100 万——五万倍的差距来自【每次砍半】。这份账就是 4-14 大 O 表的第一行实录。

五、30 秒实验

Python 标准库自带二分:import bisect; bisect.bisect_left([1,3,5,7], 5) 得 2(应插入的位置)。不用手写循环、边界零操心——实战直接用它;手写版的意义在于搞懂【砍半】。

六、常见误区

  1. 【二分查找只要数组就行】——错。有序是生命线;乱序数组二分的结果毫无意义(它信任 arr[mid] < target 就能跳半边)。先 sort 再二分,或用平衡树。
  2. 【mid = (lo + hi) // 2 天经地义】——C/Java 里有暗雷:lo+hi 相加可能整数溢出(3-12!)成负数;标准写法 mid = lo + (hi - lo) // 2。Python 整数不溢出才没事。
  3. 【二分只能找【等于】】——窄了。bisect_left/right 能找【第一个大于等于 x 的位置】——范围查询、去重插入全靠它;二分是【在有序世界定位】,不只是【找某个值】。
自测三问(点开对答案) 1. **二分查找的前提与复杂度?** 答:数据必须有序;每轮砍半,O(log n)——一百万数据约 20 轮。 2. **查找频繁时为什么【先排序再二分】划算?** 答:排序一次 O(n log n),之后每次查找 O(log n);查 k 次总账 n log n + k log n——线性则是 k x n,k 大时天差地别。 3. **什么场景二分不如哈希?** 答:只按【键精确找】且数据动态变化时——哈希 O(1) 直达;二分要维持有序(插删搬家 O(n)),平衡树又重一层。

相关笔记