第4章:线性的智慧(遍历与搜索) 兔狲教授的亲切开场 在了解了推理的边界之后,让我们回到可计算的领域,探索最简单却也最基础的搜索策略。当我们面对未知时,有两种基本态度:逐一检查的彻底,或利用结构的智能。今天,我们探索寻找的智慧。 核心议题:在未知中导航的策略是什么? 康乐园的清晨,珠江上的薄雾还未散尽。黑石屋的书房里,兔狲教授正在整理书架。阳光透过拱形窗户,在红砖墙上投下温暖的光斑。窗外,木棉花已经落尽,新叶在枝头舒展。 小小猪抱着一摞新到的书走进来,"教授,这些书要放在哪里?" 小海豹,"等等,让我看看……《算法导论》、《计算复杂性》、《哥德尔传》……我们需要一个好的分类系统。" 兔狲教授放下手中的书,微笑道:"你们提出了一个很好的问题。
:::info
兔狲教授的亲切开场
在了解了推理的边界之后,让我们回到可计算的领域,探索最简单却也最基础的搜索策略。当我们面对未知时,有两种基本态度:逐一检查的彻底,或利用结构的智能。今天,我们探索寻找的智慧。
:::
康乐园的清晨,珠江上的薄雾还未散尽。黑石屋的书房里,兔狲教授正在整理书架。阳光透过拱形窗户,在红砖墙上投下温暖的光斑。窗外,木棉花已经落尽,新叶在枝头舒展。
小小猪抱着一摞新到的书走进来,"教授,这些书要放在哪里?"
小海豹,"等等,让我看看……《算法导论》、《计算复杂性》、《哥德尔传》……我们需要一个好的分类系统。"
兔狲教授放下手中的书,微笑道:"你们提出了一个很好的问题。当我们面对一堆无序的物品——无论是书籍、信息,还是生活中的选择——我们如何有效地找到想要的东西?"
小小猪把书堆在桌上,"最简单的方法就是一本一本看呗!"
"这就是线性搜索,"兔狲教授点头,"逐一检查,确保不遗漏。但如果我们有很多很多书呢?"
小海豹思考片刻,"如果书籍是按某种顺序排列的,比如按作者姓氏字母排序,我们可以用更聪明的方法。"
"正是,"兔狲教授说,"这就是二分搜索。今天我们要探索的核心议题是:在未知中导航的策略是什么?"
他走到白板前,写下两个词:
彻底 —— 智能
"当我们寻找某物时,"兔狲教授解释,"有两种基本策略:
"这两种策略代表了两种不同的认知态度,也对应着两种不同的时间代价。"
小小猪拿起一本书,"假设我要在这堆书里找到《算法导论》。如果书是无序的,我只能一本一本看。"
"让我们形式化这个问题,"兔狲教授在白板上写下:
问题:在包含n个元素的数组arr中,查找目标值target。
线性搜索算法:
for i from 0 to n-1: if arr[i] == target: return i # 找到,返回索引 return -1 # 未找到
"这个算法的时间复杂度是O(n),"兔狲教授说,"在最坏情况下,需要检查所有n个元素。"
小海豹补充道:"但它的优势是简单和通用。无论数组是否有序,无论元素是什么类型,线性搜索都能工作。"
"是的,"兔狲教授点头,"线性搜索体现了一种彻底的认知态度:不假设任何结构,不依赖任何捷径,只是老老实实地检查每一个可能性。"
:::info
兔狲教授的评语
线性搜索的哲学是彻底的诚实。它不假设世界有结构,不期待捷径,只是系统地、耐心地探索每一个可能性。这种态度在某些情况下是必要的——当世界确实无序,或者我们对其结构一无所知时。
:::
小小猪皱眉,"但是教授,每次循环都要检查i < n和arr[i] == target两个条件,能不能优化?"
"好问题,"兔狲教授说,"有一个聪明的优化:哨兵技巧(Sentinel Technique)。"
他在白板上写出改进版:
# 在数组末尾添加目标值作为哨兵 arr.append(target) i = 0 while arr[i] != target: i += 1 if i < n: # 检查是否真的找到(而不是停在哨兵) return i else: return -1
"现在每次循环只需要检查一个条件,"兔狲教授解释,"虽然渐进复杂度还是O(n),但常数因子更小。这是在彻底性框架内的微小优化。"
:::detail
进阶科普:线性搜索的算法分析
线性搜索虽然简单,但有一些有趣的数学性质:
这些分析告诉我们:当面对完全无序的数据时,线性搜索不仅是简单的选择,而且是理论上最优的选择(在最坏情况下)。
:::
"现在,让我们考虑另一种情况,"兔狲教授说,"假设书籍是按作者姓氏字母顺序排列的。我们怎么找《算法导论》?"
小海豹立刻回答:"先看中间的书。如果作者姓氏在中间书之前,就在左半边找;如果在之后,就在右半边找。重复这个过程。"
"正是二分搜索,"兔狲教授在白板上画出算法:
def binarySearch(arr, target, left, right): if left > right: return -1 # 未找到 mid = floor((left + right) / 2) if arr[mid] == target: return mid # 找到 elif arr[mid] < target: return binarySearch(arr, target, mid+1, right) # 搜索右半边 else: return binarySearch(arr, target, left, mid-1) # 搜索右半边
小小猪眼睛一亮,"每次把搜索范围减半!这样很快就能找到!"
"是的,"兔狲教授说,"二分搜索的时间复杂度是O(log n)。对于100万本书,线性搜索最多需要100万次检查,而二分搜索只需要约20次。"
他在白板上列出对比:
| 书籍数量 | 线性搜索(最坏) | 二分搜索(最坏) |
|---|---|---|
| 10 | 10次 | 4次 |
| 100 | 100次 | 7次 |
| 1,000 | 1,000次 | 10次 |
| 1,000,000 | 1,000,000次 | 20次 |
"这种差异是指数级的,"兔狲教授强调,"二分搜索体现了智能的认知态度:利用世界的结构信息,做出有根据的跳跃。"
:::info
兔狲教授的评语
二分搜索的哲学是智能的跳跃。它假设世界有秩序,利用这种秩序快速定位目标。但这种智能是有前提的:数据必须有序。如果世界无序,二分搜索不仅无效,甚至可能出错。
:::
小海豹思考片刻,"教授,如果目标值不在数组中,但我们需要知道它应该插入的位置呢?"
"这是二分搜索的一个重要变体,"兔狲教授说,"查找插入位置(Find Insertion Point)。"
他写出算法:
def findInsertionPoint(arr, target): left = 0 right = len(arr) while left < right: mid = floor((left + right) / 2) if arr[mid] < target: left = mid + 1 else: right = mid return left # 插入位置
"这个变体在很多实际应用中很有用,"兔狲教授解释,"比如维护有序列表、实现字典、数据库索引等。"
:::detail
进阶科普:二分搜索的数学原理与边界处理
二分搜索看似简单,但有很多微妙的细节:
mid = (left + right) // 2可能溢出(对于极大数组)。安全的写法是mid = left + (right - left) // 2[left, right]区间内while left <= right与while left < right的选择取决于具体问题这些细节体现了算法设计的严谨性。一个看似简单的二分搜索,如果边界处理不当,可能导致无限循环或错误结果。
:::
兔狲教授打开投影仪,两幅规整的计算图出现在屏幕上。
"这是线性搜索的正交计算图,"兔狲教授指着左边的图说,"输入数组和目标值从左端进入,经过一系列顺序检查,最终输出结果。"
小小猪盯着图,"所有检查节点在同一层级,直线连接,就像在流水线上一个个检查产品!"
"这是二分搜索的正交计算图,"兔狲教授指着右边的图说,"输入有序数组和目标值,经过中间值计算和比较,分叉为左右两个搜索方向。"
小海豹仔细观察图的层级结构,"二分搜索有多个层级,决策线有分叉,像一棵倒置的树!这种树状结构反映了它的对数复杂度。"
"正是,"兔狲教授说,"线性搜索的决策是线性的、顺序的,所有检查在同一层级;而二分搜索的决策是对数的、树状的,有多个层级和分叉。这个可视化对比揭示了两种策略的本质差异。"
"这就是正交计算图的力量,"兔狲教授总结道,"它用视觉语言告诉我们:搜索策略的选择决定了计算路径的形状。"
兔狲教授回到座位,喝了口茶。"让我们总结今天最重要的思想模型:顺序处理与分治策略。"
他在白板上画出对比表:
| 特征 | 顺序处理(线性搜索) | 分治策略(二分搜索) |
|---|---|---|
| 认知态度 | 彻底、系统、耐心 | 智能、跳跃、高效 |
| 前提条件 | 无需假设结构 | 需要有序结构 |
| 时间代价 | O(n) | O(log n) |
| 空间代价 | O(1) | O(log n)递归栈 |
| 适用场景 | 无序数据、小规模数据 | 有序数据、大规模数据 |
| 哲学意涵 | 世界的不可知论 | 世界的可知论与秩序性 |
"这两种模型不是对立的,"兔狲教授解释,"而是互补的认知工具。聪明的思考者知道何时使用彻底性,何时使用智能性。"
小海豹问:"教授,在实际问题中,我们如何选择搜索策略?"
兔狲教授在白板上画出一个选择矩阵:
是否需要多次搜索? ├── 否:数据规模小 → 线性搜索(简单直接) │ └── 数据规模大 → 考虑排序成本 │ └── 是:需要建立索引结构 ├── 静态数据 → 排序后二分搜索 ├── 动态数据 → 平衡二叉搜索树 └── 近似搜索 → 哈希表(下章讨论)
"这个选择框架告诉我们,"兔狲教授说,"没有最好的算法,只有最适合的算法。选择取决于:
:::info
兔狲教授的评语
搜索策略的选择反映了我们对世界的理解程度和预期。如果我们对世界一无所知,线性搜索是诚实的选择;如果我们相信世界有秩序,二分搜索是高效的选择。真正的智慧在于知道:我们处在哪种情境中。
:::
"让我们用代码来比较线性搜索和二分搜索,"兔狲教授说,"从理论到实践,从时间复杂度到实际性能。"
def linear_search(arr, target): """线性搜索:逐一检查每个元素 问题描述: 在一个数组中查找目标值,返回其索引。 如果目标值不存在,返回-1。 算法思路: 1. 从数组的第一个元素开始 2. 依次检查每个元素是否等于目标值 3. 如果找到,返回当前索引 4. 如果遍历完所有元素都没找到,返回-1 关键特点: - 适用于有序和无序数组 - 实现简单直观 - 最坏情况需要检查所有元素 参数: arr: 数组(可以是有序或无序) target: 要查找的目标值 返回: 目标值在数组中的索引,如果不存在则返回-1 时间复杂度: O(n) - 最坏情况需要检查n个元素 空间复杂度: O(1) - 只使用常数额外空间 示例: >>> linear_search([3, 1, 4, 1, 5], 4) 2 >>> linear_search([3, 1, 4, 1, 5], 6) -1 """ for i, value in enumerate(arr): if value == target: return i return -1 def linear_search_with_count(arr, target): """线性搜索(带比较次数计数) 返回: (索引, 比较次数) """ count = 0 for i, value in enumerate(arr): count += 1 if value == target: return i, count return -1, count
def binary_search_iterative(arr, target): """二分搜索(迭代版):利用有序性快速定位 问题描述: 在一个已排序的数组中查找目标值,返回其索引。 如果目标值不存在,返回-1。 算法思路(分治策略): 1. 初始化左右指针指向数组两端 2. 当左指针 <= 右指针时循环: a. 计算中间位置 mid = (left + right) // 2 b. 如果 arr[mid] == target,找到目标,返回mid c. 如果 arr[mid] < target,目标在右侧,移动左指针到mid+1 d. 如果 arr[mid] > target,目标在左侧,移动右指针到mid-1 3. 循环结束未找到,返回-1 关键特点: - 要求数组必须已排序 - 每次比较将搜索范围减半 - 效率远高于线性搜索(对数级 vs 线性级) 前提条件: 数组必须已排序(升序或降序) 参数: arr: 已排序的数组(通常为升序) target: 要查找的目标值 返回: 目标值在数组中的索引,如果不存在则返回-1 时间复杂度: O(log n) - 每次比较将搜索范围减半 空间复杂度: O(1) - 只使用常数额外空间 示例: >>> binary_search_iterative([1, 3, 5, 7, 9], 5) 2 >>> binary_search_iterative([1, 3, 5, 7, 9], 6) -1 >>> binary_search_iterative([], 5) -1 注意: - 如果数组有重复元素,返回的索引不一定是第一个出现的位置 - 对于降序数组,需要调整比较逻辑 """ 空间复杂度: O(1) """ 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 binary_search_iterative_with_count(arr, target): """二分搜索(迭代版,带比较次数计数) 返回: (索引, 比较次数) """ left, right = 0, len(arr) - 1 count = 0 while left <= right: count += 1 mid = left + (right - left) // 2 if arr[mid] == target: return mid, count elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1, count
def binary_search_recursive(arr, target, left=0, right=None): """二分搜索(递归版):体现分治思想 参数: arr: 已排序的数组 target: 要查找的目标值 left: 搜索范围的左边界 right: 搜索范围的右边界 返回: 目标值在数组中的索引,如果不存在则返回-1 时间复杂度: 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)
import time import random def search_performance_experiment(): """比较不同搜索算法的性能""" print("搜索算法性能对比实验") print("=" * 50) # 测试数据规模 sizes = [1000, 10000, 100000, 1000000] for size in sizes: print(f"\n数据规模: {size}") # 生成测试数据 sorted_data = list(range(size)) target_in_middle = size // 2 # 目标在中间 target_not_exist = -1 # 不存在的目标 # 测试1:有序数组中的搜索(目标存在) print(f" 测试1:在有序数组中查找存在的目标(位置{target_in_middle})") # 二分搜索 start = time.time() idx1 = binary_search_iterative(sorted_data, target_in_middle) time1 = time.time() - start # 线性搜索 start = time.time() idx2 = linear_search(sorted_data, target_in_middle) time2 = time.time() - start print(f" 二分搜索: {time1:.6f}秒, 结果: {idx1}") print(f" 线性搜索: {time2:.6f}秒, 结果: {idx2}") print(f" 速度比: {time2/time1:.1f}倍" if time1 > 0 else " 速度比: 无限倍") # 测试2:有序数组中的搜索(目标不存在) print(f" 测试2:在有序数组中查找不存在的目标") # 二分搜索(目标不存在) start = time.time() idx3 = binary_search_iterative(sorted_data, target_not_exist) time3 = time.time() - start # 线性搜索(目标不存在) start = time.time() idx4 = linear_search(sorted_data, target_not_exist) time4 = time.time() - start print(f" 二分搜索: {time3:.6f}秒, 结果: {idx3}") print(f" 线性搜索: {time4:.6f}秒, 结果: {idx4}") print(f" 速度比: {time4/time3:.1f}倍" if time3 > 0 else " 速度比: 无限倍") # 测试3:无序数组中的搜索 print(f" 测试3:在无序数组中查找(需要先排序)") # 生成无序数据 unsorted_data = list(range(size)) random.shuffle(unsorted_data) target = random.randint(0, size - 1) # 方案A:先排序再二分搜索 start = time.time() sorted_copy = sorted(unsorted_data) # O(n log n) idx_a = binary_search_iterative(sorted_copy, target) # O(log n) time_a = time.time() - start # 方案B:直接线性搜索 start = time.time() idx_b = linear_search(unsorted_data, target) # O(n) time_b = time.time() - start print(f" 先排序再二分: {time_a:.6f}秒, 结果: {idx_a}") print(f" 直接线性搜索: {time_b:.6f}秒, 结果: {idx_b}") # 决策分析 if time_a < time_b: print(f" 结论: 先排序再搜索更快(差{time_b-time_a:.6f}秒)") else: print(f" 结论: 直接线性搜索更快(差{time_a-time_b:.6f}秒)") # 运行性能实验 search_performance_experiment()
def search_strategy_selection(data_size, query_count, is_sorted, memory_constraint): """搜索策略选择框架 参数: data_size: 数据规模 query_count: 查询次数 is_sorted: 数据是否已排序 memory_constraint: 内存约束(MB) 返回: 推荐的搜索策略 """ print("\n" + "=" * 50) print("搜索策略选择框架") print("=" * 50) print(f"问题特征:") print(f" 数据规模: {data_size}") print(f" 查询次数: {query_count}") print(f" 是否已排序: {is_sorted}") print(f" 内存约束: {memory_constraint}MB") print("\n策略分析:") # 计算各种策略的时间复杂度 # 简化估计:假设每次操作耗时1单位 linear_time = data_size * query_count # O(n * q) if is_sorted: binary_time = query_count * (data_size.bit_length()) # O(q * log n) print(f" 1. 二分搜索: O(q log n) ≈ {binary_time} 单位时间") else: # 先排序再二分 sort_time = data_size * (data_size.bit_length()) # O(n log n) 排序 binary_time = query_count * (data_size.bit_length()) # O(q log n) 搜索 total_sorted_time = sort_time + binary_time print(f" 1. 先排序再二分: O(n log n + q log n) ≈ {total_sorted_time} 单位时间") print(f" 2. 线性搜索: O(nq) ≈ {linear_time} 单位时间") # 策略推荐 print("\n策略推荐:") if query_count == 1: print(" - 单次查询 → 线性搜索通常更简单") if not is_sorted: print(" - 注意:对于单次查询,排序的成本可能超过收益") elif query_count > 1 and is_sorted: print(" - 多次查询 + 已排序 → 二分搜索是最佳选择") expected_speedup = linear_time / binary_time if binary_time > 0 else float('inf') print(f" - 预期加速比: {expected_speedup:.1f}倍") elif query_count > 1 and not is_sorted: print(" - 多次查询 + 未排序 → 考虑先排序再二分") # 比较两种策略 if total_sorted_time < linear_time: speedup = linear_time / total_sorted_time print(f" - 先排序再二分比线性搜索快 {speedup:.1f}倍") else: print(" - 查询次数不够多,直接线性搜索可能更快") # 内存考虑 if memory_constraint < 10: # 内存紧张 print("\n内存考虑:") print(" - 内存紧张时,避免需要额外存储的算法") print(" - 二分搜索(迭代版)只需要O(1)额外空间") print(" - 归并排序需要O(n)额外空间,考虑原地排序算法") print("\n兔狲教授的智慧总结:") print("1. 线性搜索:体现彻底的认知态度,简单通用") print("2. 二分搜索:体现智能的认知态度,需要有序数据") print("3. 策略选择取决于:数据特征、查询模式、资源约束") print("4. 有时,先排序再搜索比直接线性搜索更高效") # 应用策略选择框架 print("\n" + "=" * 50) print("搜索策略选择示例") print("=" * 50) # 示例1:电话号码查询系统 print("\n示例1:电话号码查询系统") search_strategy_selection( data_size=10000, # 1万个联系人 query_count=1000, # 每天1000次查询 is_sorted=True, # 已按姓名排序 memory_constraint=100 # 100MB内存 ) # 示例2:临时数据查找 print("\n示例2:临时数据查找") search_strategy_selection( data_size=100, # 100个临时数据 query_count=1, # 只查一次 is_sorted=False, # 未排序 memory_constraint=10 # 10MB内存 )
def boundary_test_suite(): """边界测试套件:测试搜索算法在各种边界情况下的表现""" print("\n" + "=" * 50) print("边界测试套件") print("=" * 50) test_cases = [ # (描述, 数组, 目标值, 期望结果) ("空数组", [], 5, -1), ("单元素数组(存在)", [5], 5, 0), ("单元素数组(不存在)", [5], 3, -1), ("两个元素(目标在开头)", [2, 5], 2, 0), ("两个元素(目标在结尾)", [2, 5], 5, 1), ("两个元素(目标不存在)", [2, 5], 3, -1), ("多个元素(目标在开头)", [1, 3, 5, 7, 9], 1, 0), ("多个元素(目标在中间)", [1, 3, 5, 7, 9], 5, 2), ("多个元素(目标在结尾)", [1, 3, 5, 7, 9], 9, 4), ("多个元素(目标不存在,在范围内)", [1, 3, 5, 7, 9], 4, -1), ("多个元素(目标不存在,小于最小值)", [1, 3, 5, 7, 9], 0, -1), ("多个元素(目标不存在,大于最大值)", [1, 3, 5, 7, 9], 10, -1), ("有重复元素(找到第一个)", [1, 2, 2, 2, 3], 2, 1), # 二分搜索不一定返回第一个 ] print("测试二分搜索(迭代版):") passed = 0 total = len(test_cases) for desc, arr, target, expected in test_cases: result = binary_search_iterative(arr, target) status = "✓" if result == expected else "✗" if status == "✓": passed += 1 # 对于有重复元素的特殊情况,二分搜索可能返回任意一个匹配位置 if desc == "有重复元素(找到第一个)": if result in [1, 2, 3]: # 只要找到任意一个2就是正确的 status = "✓" if status == "✓": passed += 1 # 这里逻辑有点问题,但先这样 print(f" {status} {desc}: arr={arr}, target={target}, result={result}, expected={expected}") print(f"\n通过率: {passed}/{total} ({passed/total*100:.1f}%)") print("\n算法严谨性要点:") print("1. 空数组处理:left > right 条件是否正确处理?") print("2. 单元素数组:mid计算是否正确?") print("3. 边界条件:left=right时循环是否继续?") print("4. 溢出问题:mid = (left + right) // 2 可能溢出,应该用 left + (right - left) // 2") print("5. 重复元素:二分搜索不一定返回第一个匹配项,需要特别处理") # 运行边界测试 boundary_test_suite() print("\n" + "=" * 50) print("搜索算法思考题:") print("1. 如果数据是链表而不是数组,二分搜索还适用吗?为什么?") print("2. 当数据频繁插入/删除时,应该选择哪种搜索策略?") print("3. 二分搜索的递归版和迭代版各有什么优缺点?")
"通过这些代码,"兔狲教授总结道,"我们不仅学会了两种搜索算法,更理解了在未知中导航的策略。线性搜索体现彻底的认知态度,二分搜索体现智能的认知态度。真正的智慧在于知道:我们处在哪种情境中。"
:::info
兔狲教授的总结:导航未知的两种智慧
算法实现:分别实现线性搜索和二分搜索。测试它们在有序数组、无序数组、不同数据规模下的性能。记录实际运行时间与理论分析的差异。
边界测试:设计测试用例检查二分搜索的边界处理:空数组、单元素数组、目标在开头、目标在结尾、目标不存在等。体会算法严谨性的重要性。
实际应用:在手机通讯录中找联系人,手机用的是哪种搜索策略?观察搜索框的实时提示功能,猜测背后的实现原理。
算法溯源:二分搜索的思想最早出现在哪里?查阅历史文献,看看古代是否有类似的"折半查找"思想(如中国的"对分法"求平方根)。
复杂度演进:从线性搜索到二分搜索,再到后来的哈希表、B树、跳表等,搜索算法如何随着计算机硬件和数据规模的发展而演进?
文化影响:"二分法"思维如何影响了西方哲学和科学方法论?(提示:笛卡尔的"分析-综合"方法)
哲学反思:线性搜索的"彻底性"与二分搜索的"智能性"反映了两种认识论态度。在科学探索中,什么时候应该"逐一检查",什么时候可以"大胆假设"?
伦理考量:在信息检索系统中,快速搜索可能带来信息茧房——系统只展示我们可能喜欢的内容。如何在效率与多样性之间取得平衡?
创造练习:设计一个"搜索寓言"——用故事解释线性搜索和二分搜索的区别。(提示:可以比喻为在图书馆找书的两种方式)
极限挑战:如果数据是部分有序的(如旋转有序数组),如何修改二分搜索算法?这对应着现实世界中怎样的认知情境?
午后的阳光透过榕树的枝叶,在黑石屋的地板上投下斑驳的光影。珠江上传来轮船的汽笛声,悠长而遥远。
小小猪正在电脑上测试不同搜索算法的性能,屏幕上跳动着时间测量数据。小海豹则在白板上画着搜索决策树,试图理解那对数增长的奥秘。
"今天我们从'在未知中导航的策略'开始,"兔狲教授收拾着茶具,"探索了线性搜索的彻底性与二分搜索的智能性。"
小海豹放下笔,"教授,如果数据是动态变化的——不断有新的书加入,旧的书移走——二分搜索还适用吗?"
兔狲教授微笑:"好问题。这就需要更复杂的数据结构了,比如平衡二叉搜索树。"
小小猪抬头,"还有哈希表!我记得哈希表查找更快,O(1)时间!"
"是的,"兔狲教授点头,"但哈希表有它自己的代价和限制。下一章,我们要探索贪心算法——一种看似简单却充满智慧的决策策略。"
"贪心?"小小猪好奇,"就像每次选最大的硬币?"
"类似,"兔狲教授说,"贪心算法在每一步都做出局部最优的选择,希望这些选择能导向全局最优。但有时候,这种'短视'的策略会失败……"
窗外,康乐园的钟声响起,惊起一群白鸽。鸽群在空中划出优美的弧线,仿佛在搜索着回家的路。
小小猪的笔记:我测试了线性搜索和二分搜索。100万个有序数中找目标,线性搜索平均0.5秒,二分搜索只要0.00001秒!5万倍的差距!理论上的对数增长在现实中如此惊人。
小海豹的笔记:查了二分搜索的历史,发现1946年约翰·冯·诺依曼就提到了类似思想,但第一个公开发表的二分搜索算法是1949年的。有时,简单想法的形式化也需要时间。
兔狲教授的结语:记住,搜索策略的选择反映了我们对问题的理解程度。彻底有彻底的价值,智能有智能的前提。知道何时用哪种策略,是算法思维成熟的标志。我们慢慢来,理解了最重要。