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)
插入删除 困难(需移动元素) 容易(块内操作) 最容易
有序性输出 支持 块内不支持 不支持
范围查询 支持 支持(确定块范围) 不支持
适用场景 静态有序,频繁查找 动态数据,块内无序 无需有序,极致速度
相关推荐
大圣编程9 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
云烟成雨TD10 小时前
LangFlow 1.x 系列【5】可视化编辑页面功能说明
人工智能·python·agent
geovindu11 小时前
python: Functional Options Pattern
开发语言·后端·python·设计模式·惯用法模式·函数式选项模式
tryCbest11 小时前
Python 文件操作
服务器·python
涛声依旧-底层原理研究所12 小时前
Agent 长任务可靠性设计:实现暂停、恢复、续跑与崩溃重启的完整方案
人工智能·python·系统架构
AC赳赳老秦12 小时前
防火墙规则批量配置实战:OpenClaw 自动生成模板、批量下发与合规性校验全解析
java·开发语言·人工智能·python·github·php·openclaw
小小编程路12 小时前
如何优化while循环的性能?
python
lzqrzpt13 小时前
LED驱动电源选型标准与工程应用技术要点解析
python·单片机·嵌入式硬件·物联网
Maiko Star13 小时前
Python核心语法——函数
开发语言·python
linzᅟᅠ13 小时前
README
人工智能·python