Python 算法基础篇之查找算法(二):斐波那契查找、分块查找与哈希查找

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)
插入删除 困难(需移动元素) 容易(块内操作) 最容易
有序性输出 支持 块内不支持 不支持
范围查询 支持 支持(确定块范围) 不支持
适用场景 静态有序,频繁查找 动态数据,块内无序 无需有序,极致速度
相关推荐
牙牙要健康7 小时前
Windows 下为 VSCode 配置 Anaconda:从零安装 Python 环境到完整配置教程
windows·vscode·python
财经资讯数据_灵砚智能7 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年5月17日
大数据·人工智能·python·信息可视化·自然语言处理
databook7 小时前
切线的魔法:用 SymPy 和 Manim 轻松搞定导数动画
python·数学·动效
程序员榴莲7 小时前
Python 正则表达式入门:从匹配手机号到提取文本内容
python·正则表达式
程序员榴莲8 小时前
Python 中的 @property:像访问属性一样调用方法
开发语言·前端·python
坐吃山猪8 小时前
【Nanobot】README04_LEVEL2 提供商系统设计
python·源码·agent·nanobot
坐吃山猪8 小时前
【Nanobot】README09_LEVEL4 添加新聊天渠道
开发语言·网络·python·源码·nanobot
Mr.朱鹏8 小时前
9-检索增强生成RAG详解
python·gpt·langchain·大模型·llm·rag
shehuiyuelaiyuehao8 小时前
算法27,二维前缀和
开发语言·python·算法