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