1. 回顾与进阶:从二分查找到更优策略
1.1 第一篇回顾
| 算法 | 时间复杂度 | 核心思想 | 局限性 |
|---|---|---|---|
| 顺序查找 | O(n) | 逐个遍历 | 数据量大时极慢 |
| 二分查找 | O(log n) | 折半缩小范围 | 要求有序,插值点固定 |
| 插值查找 | O(log log n)~O(n) | 按比例预测位置 | 数据不均匀时退化 |
1.2 新的问题场景
场景1:数据量大且有序,但频繁插入删除
→ 二分查找的静态数组难以维护
场景2:数据分布极不均匀,插值查找失效
→ 需要更稳定的分割策略
场景3:数据无需有序,要求O(1)查找
→ 能否用空间换时间?
本篇解决的三个算法:
| 算法 | 解决的核心问题 | 关键特性 |
|---|---|---|
| 斐波那契查找 | 二分查找的mid点选择不够优雅 | 黄金分割比例,减少比较次数 |
| 分块查找 | 大数据集的维护与查找平衡 | 块内无序,块间有序,动态友好 |
| 哈希查找 | 打破"有序"的束缚 | O(1)平均时间,空间换时间 |
2. 斐波那契查找:黄金分割的艺术
2.1 为什么叫斐波那契查找?
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144...
特性:F(k) = F(k-1) + F(k-2),且 F(k-1)/F(k) ≈ 0.618(黄金分割比)
核心思想 :二分查找总是从中间分割(1:1),而斐波那契查找按黄金分割比例分割(约 0.618:0.382 ),使得较长的一段在左侧,减少高端元素的比较次数(假设低端元素查找概率更高)。
分割比例对比:
二分查找: 斐波那契查找:
0.5 : 0.5 0.618 : 0.382
┌────┬────┐ ┌────────┬────┐
│左半│右半│ │ 左半 │右半│
│ 50%│ 50%│ │ 61.8% │38.2%│
└────┴────┘ └────────┴────┘
对称分割 非对称分割(黄金分割)
2.2 算法原理详解
关键性质:如果数组长度为 F(k)-1,则可以分割为:
-
左半部分长度:F(k-1)-1
-
右半部分长度:F(k-2)-1
-
中间元素:F(k-1) 位置
斐波那契分割示意(以 F(6)=8 为例):
数组长度 = F(6) - 1 = 7
索引: 0 1 2 3 4 5 6
├───────────────┼───────────┤
│ F(5)-1=4 │ F(4)-1=2 │
│ 左半部分 │ 右半部分 │
└───────────────┴───────────┘
↑
mid = F(5)-1 = 4(从0开始计数)分割后:
- 左半:索引 0~3,长度 4 = F(5)-1
- 右半:索引 5~6,长度 2 = F(4)-1
2.3 完整代码实现
python
def fibonacci_search(arr: list, target) -> int:
"""
斐波那契查找
前提:arr 必须是有序的(升序)
参数:
arr: 已排序列表
target: 目标值
返回:
目标值的索引,未找到返回 -1
时间复杂度:O(log n)
空间复杂度:O(1)
特点:
- 分割比例接近黄金分割 0.618:0.382
- 仅使用加减运算,不涉及乘除(早期计算机乘除慢)
- 当数据在左侧概率更高时,平均性能略优于二分
"""
n = len(arr)
if n == 0:
return -1
# 步骤1:生成足够的斐波那契数列
fib = [0, 1]
while fib[-1] < n:
fib.append(fib[-1] + fib[-2])
# 步骤2:找到最小的 k,使得 F(k) >= n+1
k = len(fib) - 1
# 步骤3:如果 F(k)-1 > n,用最后一个元素填充数组至长度 F(k)-1
# 创建临时数组(不修改原数组)
temp = arr + [arr[-1]] * (fib[k] - 1 - n)
# 步骤4:斐波那契查找
left, right = 0, fib[k] - 2 # 有效范围是 0 到 F(k)-2
while left <= right:
# 计算 mid:left + F(k-1) - 1
# 即左半部分的最后一个元素
mid = left + fib[k - 1] - 1
if temp[mid] == target:
# 如果 mid 在原数组范围内,返回
if mid < n:
return mid
# 如果 mid 是填充区域,说明 target 就是 arr[-1]
return n - 1
elif temp[mid] < target:
# 目标在右半部分
# 右半部分长度 = F(k-2)-1
left = mid + 1
k -= 2 # 更新 k
else:
# 目标在左半部分
# 左半部分长度 = F(k-1)-1
right = mid - 1
k -= 1 # 更新 k
return -1
# 测试
sorted_data = [10, 22, 35, 47, 59, 62, 78, 88, 95, 100, 105, 110]
print(fibonacci_search(sorted_data, 62)) # 5
print(fibonacci_search(sorted_data, 10)) # 0(第一个元素)
print(fibonacci_search(sorted_data, 110)) # 11(最后一个元素)
print(fibonacci_search(sorted_data, 50)) # -1(不存在)
2.4 查找过程可视化
数据:[10, 22, 35, 47, 59, 62, 78, 88, 95, 100, 105, 110]
目标:62
长度 n=12
步骤1:生成斐波那契数列
F = [0, 1, 1, 2, 3, 5, 8, 13, 21, ...]
找到 F(7)=13 >= 12+1=13,所以 k=7
步骤2:填充数组至 F(7)-1 = 12 个元素
原数组已有12个,无需填充
(如果 n=11,则需要填充1个 arr[-1])
步骤3:开始查找
初始:left=0, right=11, k=7
第1轮:
mid = 0 + F(6) - 1 = 0 + 8 - 1 = 7
temp[7] = 88
88 > 62?是,目标在左半部分
right = 7 - 1 = 6
k = 7 - 1 = 6
当前范围:[10, 22, 35, 47, 59, 62, 78]
0 1 2 3 4 5 6
第2轮:
mid = 0 + F(5) - 1 = 0 + 5 - 1 = 4
temp[4] = 59
59 < 62?是,目标在右半部分
left = 4 + 1 = 5
k = 6 - 2 = 4
当前范围:[62, 78]
5 6
第3轮:
mid = 5 + F(3) - 1 = 5 + 2 - 1 = 6
temp[6] = 78
78 > 62?是,目标在左半部分
right = 6 - 1 = 5
k = 4 - 1 = 3
当前范围:[62]
5
第4轮:
mid = 5 + F(2) - 1 = 5 + 1 - 1 = 5
temp[5] = 62
62 == 62?✅ 找到!索引=5
共比较4次(二分查找也需要约4次)
2.5 斐波那契查找 vs 二分查找
| 对比维度 | 斐波那契查找 | 二分查找 |
|---|---|---|
| 分割比例 | 0.618 : 0.382(黄金分割) | 0.5 : 0.5(对称) |
| mid计算 | 加减法(F(k-1)-1) | 位运算或加减除法 |
| 平均比较次数 | 略少(假设低端查找概率高) | 稳定 |
| 最坏情况 | O(log n) | O(log n) |
| 适用场景 | 有序静态数据,频繁查找低端 | 通用有序数据 |
| 现代CPU性能 | 无明显优势(乘除法已很快) | 更直观,更常用 |
💡 现代观点 :斐波那契查找在理论上优雅,但在现代计算机上,由于CPU乘除法指令已高度优化,其实际性能优势很小。主要价值在于理解黄金分割在算法设计中的应用。
3. 分块查找:索引+顺序的折中方案
3.1 为什么需要分块查找?
问题场景 :数据量很大(百万级),且频繁插入删除,无法保持全局有序。
- 顺序查找:太慢,O(n)
- 二分查找:要求有序,插入删除代价高
- 哈希查找:需要额外空间,且无序
分块查找的思想 :将数据分成若干块,块内无序,块间有序。建立索引表记录每块的最大值和起始位置。
分块查找的结构:
索引表(有序) 数据块(块内无序)
┌─────────┬────────┐ ┌─────────────────────────────┐
│ 块最大值 │ 起始位置 │ │ Block 0 (最大值 35) │
├─────────┼────────┤ │ [22, 10, 35, 18] │
│ 35 │ 0 │ ├─────────────────────────────┤
│ 72 │ 4 │ │ Block 1 (最大值 72) │
│ 95 │ 8 │ │ [56, 72, 48, 60] │
│ 110 │ 12 │ ├─────────────────────────────┤
└─────────┴────────┘ │ Block 2 (最大值 95) │
│ [88, 95, 78, 80] │
├─────────────────────────────┤
│ Block 3 (最大值 110) │
│ [100, 105, 110, 102] │
└─────────────────────────────┘
查找 60 的过程:
1. 索引表二分查找:60 在 35 和 72 之间 → Block 1
2. 在 Block 1 中顺序查找:找到 60,索引=6
3.2 完整代码实现
python
class BlockSearch:
"""
分块查找实现
结构:
- 索引表:记录每块的最大值和起始/结束位置
- 数据块:块内无序,块间有序(前块max < 后块min)
"""
def __init__(self):
self.index_table = [] # [(max_val, start, end), ...]
self.data = [] # 实际数据
self.block_size = 0
def build(self, arr: list, block_size: int = None):
"""
构建分块结构
参数:
arr: 原始数据列表
block_size: 每块大小,默认 √n
"""
if not arr:
return
self.data = arr.copy()
n = len(arr)
# 最优块大小:√n
if block_size is None:
block_size = int(n ** 0.5)
self.block_size = block_size
# 构建索引表
self.index_table = []
for i in range(0, n, block_size):
block = arr[i:i + block_size]
max_val = max(block)
self.index_table.append({
'max': max_val,
'start': i,
'end': min(i + block_size - 1, n - 1)
})
# 确保块间有序(前块max <= 后块min)
# 如果原始数据不满足,需要排序后分块
# 这里假设输入已满足或用户已处理
def search(self, target) -> int:
"""
分块查找
返回:目标值的索引,未找到返回 -1
"""
if not self.data:
return -1
# 步骤1:在索引表中确定目标所在的块
block_idx = self._find_block(target)
if block_idx == -1:
return -1
# 步骤2:在确定的块内顺序查找
block_info = self.index_table[block_idx]
start, end = block_info['start'], block_info['end']
for i in range(start, end + 1):
if self.data[i] == target:
return i
return -1
def _find_block(self, target) -> int:
"""
在索引表中查找目标应该所在的块
索引表按max有序,可以用二分或顺序查找
"""
# 顺序查找索引表(索引表通常很小)
for i, block in enumerate(self.index_table):
if target <= block['max']:
return i
# 如果比所有块的max都大,不存在
return -1
def insert(self, value):
"""
插入元素(插入到合适的块,或新建块)
分块查找的优势:插入不需要移动大量元素!
"""
# 找到应该插入的块
block_idx = self._find_block(value)
if block_idx == -1:
# 比所有元素都大,追加到最后
self.data.append(value)
# 更新最后一块或新建块
if len(self.index_table) == 0:
self.index_table.append({
'max': value,
'start': 0,
'end': 0
})
else:
last_block = self.index_table[-1]
if last_block['end'] - last_block['start'] + 1 < self.block_size:
# 最后一块未满,直接加入
last_block['max'] = max(last_block['max'], value)
last_block['end'] += 1
else:
# 新建块
self.index_table.append({
'max': value,
'start': last_block['end'] + 1,
'end': last_block['end'] + 1
})
else:
# 插入到指定块
block = self.index_table[block_idx]
insert_pos = block['end'] + 1
# 数据插入(列表insert是O(n),但块内数据少)
self.data.insert(insert_pos, value)
# 更新当前块及后续块的索引
block['max'] = max(block['max'], value)
block['end'] += 1
for i in range(block_idx + 1, len(self.index_table)):
self.index_table[i]['start'] += 1
self.index_table[i]['end'] += 1
def display(self):
"""显示分块结构"""
print("索引表:")
for i, idx in enumerate(self.index_table):
print(f" Block {i}: max={idx['max']}, range=[{idx['start']}, {idx['end']}]")
print("\n数据块:")
for i, idx in enumerate(self.index_table):
block_data = self.data[idx['start']:idx['end']+1]
print(f" Block {i}: {block_data}")
# 测试
bs = BlockSearch()
# 构建数据(已按块间有序组织)
data = [22, 10, 35, 18, # Block 0, max=35
56, 48, 72, 60, # Block 1, max=72
88, 78, 95, 80, # Block 2, max=95
100, 102, 105, 110] # Block 3, max=110
bs.build(data, block_size=4)
bs.display()
print(f"\n查找 60: 索引={bs.search(60)}") # 7
print(f"查找 95: 索引={bs.search(95)}") # 10
print(f"查找 50: 索引={bs.search(50)}") # -1
# 测试插入
bs.insert(58)
print(f"\n插入 58 后查找: 索引={bs.search(58)}")
bs.display()
3.3 分块查找的适用场景
python
# ✅ 适用场景
# 1. 数据库索引(简化版)
class DatabaseIndex:
"""模拟数据库的分块索引"""
def __init__(self):
self.blocks = [] # 数据块
self.index = [] # 索引
def add_record(self, record_id, data):
# 找到合适的块插入
pass
def range_query(self, start_id, end_id):
"""
范围查询:分块查找的优势!
确定起始块和结束块,只扫描相关块
"""
start_block = self._find_block(start_id)
end_block = self._find_block(end_id)
results = []
for i in range(start_block, end_block + 1):
for record in self.blocks[i]:
if start_id <= record['id'] <= end_id:
results.append(record)
return results
# 2. 文件系统的块索引
# 操作系统将文件分成块,通过索引节点(inode)管理
# 3. 大规模日志查询
# 日志按时间分块,先确定时间范围(块),再块内查找
4. 哈希查找:O(1)的时间复杂度神话
4.1 从"有序查找"到"直接定位"
哈希查找(Hash Search) :通过哈希函数将键直接映射到存储位置,实现**O(1)**平均时间复杂度的查找。
查找方式演进:
顺序查找:逐个比较 → O(n)
│
▼
二分查找:有序折半 → O(log n)
│
▼
哈希查找:计算位置 → O(1) ⭐
核心思想:用"计算"替代"比较"
4.2 哈希表的核心结构
哈希表结构:
键(key) → 哈希函数 → 哈希值 → 取模 → 数组索引
↓
┌─────────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
└─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┘
│ │ │ │ │ │ │ │
None 'a' None 'b' 'c' None 'd' None
查找 'c':
hash('c') % 8 = 4 → 直接访问索引4 → O(1)
4.3 完整哈希表实现(含冲突处理)
python
class HashTable:
"""
哈希表实现:拉链法解决冲突
冲突(Collision):不同键映射到同一索引
解决方法:
- 拉链法(Chaining):每个槽位存链表
- 开放寻址法(Open Addressing):线性探测、二次探测
"""
def __init__(self, capacity=16):
self.capacity = capacity
self.size = 0
self.table = [[] for _ in range(capacity)] # 拉链法:每个槽是列表
self.load_factor_threshold = 0.75 # 扩容阈值
def _hash(self, key) -> int:
"""哈希函数:将键映射为整数"""
# Python内置hash(),对字符串/数字有效
# 自定义对象需要实现__hash__
return hash(key) % self.capacity
def put(self, key, value):
"""
插入/更新键值对
时间复杂度:O(1) 平均,O(n) 最坏(所有键冲突)
"""
# 检查扩容
if self.size / self.capacity > self.load_factor_threshold:
self._resize()
index = self._hash(key)
bucket = self.table[index]
# 查找是否已存在
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value) # 更新
return
# 不存在,追加
bucket.append((key, value))
self.size += 1
def get(self, key):
"""
查找键对应的值
时间复杂度:O(1) 平均,O(n) 最坏
"""
index = self._hash(key)
bucket = self.table[index]
for k, v in bucket:
if k == key:
return v
raise KeyError(f"Key {key} not found")
def contains(self, key) -> bool:
"""判断是否包含键"""
try:
self.get(key)
return True
except KeyError:
return False
def remove(self, key):
"""删除键值对"""
index = self._hash(key)
bucket = self.table[index]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
self.size -= 1
return v
raise KeyError(f"Key {key} not found")
def _resize(self):
"""扩容:容量翻倍,重新哈希所有元素"""
old_table = self.table
self.capacity *= 2
self.size = 0
self.table = [[] for _ in range(self.capacity)]
for bucket in old_table:
for key, value in bucket:
self.put(key, value) # 重新插入
def display(self):
"""显示哈希表结构"""
for i, bucket in enumerate(self.table):
if bucket:
print(f"Slot {i}: {bucket}")
def __len__(self):
return self.size
def __repr__(self):
items = []
for bucket in self.table:
items.extend(bucket)
return f"HashTable({dict(items)})"
# 测试
ht = HashTable(8)
# 插入
ht.put("apple", 5)
ht.put("banana", 3)
ht.put("cherry", 8)
ht.put("date", 2)
ht.put("elderberry", 7)
print("哈希表内容:")
ht.display()
print(f"\n查找 'cherry': {ht.get('cherry')}") # 8
print(f"包含 'banana': {ht.contains('banana')}") # True
# 更新
ht.put("apple", 10)
print(f"更新后 'apple': {ht.get('apple')}") # 10
# 删除
ht.remove("date")
print(f"删除后包含 'date': {ht.contains('date')}") # False
# 触发扩容
for i in range(20):
ht.put(f"key{i}", i)
print(f"\n扩容后大小: {len(ht)}, 容量: {ht.capacity}")
4.4 冲突处理详解
冲突场景可视化:
插入 "abc" 和 "cba",假设它们哈希值相同:
拉链法(Chaining):
┌─────┐
│ 3 │ ──→ [("abc", 100)] ──→ [("cba", 200)] ──→ None
└─────┘
冲突时用链表连接
开放寻址法(Open Addressing):
┌─────┐
│ 3 │ → ("abc", 100) ← 原始位置
└─────┘
│ 4 │ → ("cba", 200) ← 冲突,探测到下一个空位
└─────┘
python
class HashTableOpenAddressing:
"""
开放寻址法实现:线性探测
优势:缓存友好,无指针开销
劣势:删除复杂(需要标记删除),容易聚集
"""
DELETED = object() # 删除标记
def __init__(self, capacity=16):
self.capacity = capacity
self.size = 0
self.table = [None] * capacity
def _hash(self, key, probe=0):
"""线性探测哈希"""
return (hash(key) + probe) % self.capacity
def put(self, key, value):
for i in range(self.capacity):
index = self._hash(key, i)
# 空位或删除标记,可以插入
if self.table[index] is None or self.table[index] is self.DELETED:
self.table[index] = (key, value)
self.size += 1
return
# 已存在,更新
elif self.table[index][0] == key:
self.table[index] = (key, value)
return
# 表满,扩容
self._resize()
self.put(key, value)
def get(self, key):
for i in range(self.capacity):
index = self._hash(key, i)
if self.table[index] is None:
return None # 遇到空位,说明不存在
if self.table[index] is not self.DELETED and self.table[index][0] == key:
return self.table[index][1]
return None
def remove(self, key):
for i in range(self.capacity):
index = self._hash(key, i)
if self.table[index] is None:
return False
if self.table[index] is not self.DELETED and self.table[index][0] == key:
self.table[index] = self.DELETED
self.size -= 1
return True
return False
def _resize(self):
old_table = self.table
self.capacity *= 2
self.size = 0
self.table = [None] * self.capacity
for item in old_table:
if item is not None and item is not self.DELETED:
self.put(item[0], item[1])
5. 三种查找算法对比总结
5.1 核心特性对比
| 特性 | 斐波那契查找 | 分块查找 | 哈希查找 |
|---|---|---|---|
| 数据结构要求 | 有序数组 | 块间有序,块内无序 | 无要求 |
| 时间复杂度(平均) | O(log n) | O(√n) ~ O(log n) | O(1) |
| 时间复杂度(最坏) | O(log n) | O(√n) | O(n)(冲突严重) |
| 空间复杂度 | O(1) | O(√n)索引表 | O(n) |
| 插入删除 | 困难(需移动元素) | 容易(块内操作) | 最容易 |
| 有序性输出 | 支持 | 块内不支持 | 不支持 |
| 范围查询 | 支持 | 支持(确定块范围) | 不支持 |
| 适用场景 | 静态有序,频繁查找 | 动态数据,块内无序 | 无需有序,极致速度 |