1. 二分查找(Binary Search)
1.1 基本思想
在已排序数组中,每次将搜索范围减半。
def binary_search(arr, target):
"""
返回 target 的索引,不存在返回 -1
循环不变量:target 在 [left, right] 范围内
"""
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
# 找左边界(第一个 ≥ target 的位置)
def lower_bound(arr, target):
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid] < target:
left = mid + 1
else:
right = mid
return left
# 找右边界(第一个 > target 的位置)
def upper_bound(arr, target):
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid] <= target:
left = mid + 1
else:
right = mid
return left
1.2 二分查找的扩展应用
# 旋转排序数组中找最小值
def find_min_rotated(arr):
left, right = 0, len(arr) - 1
while left < right:
mid = left + (right - left) // 2
if arr[mid] > arr[right]:
left = mid + 1
else:
right = mid
return arr[left]
# 在旋转数组中搜索
def search_rotated(arr, target):
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
二分查找的精髓:不是只用于查找,而是用于求解单调性问题的最优解(如能力值验证)。
2. 哈希表(Hash Table)
2.1 核心概念
哈希表通过哈希函数将键映射到数组索引,实现 O(1) 的平均查找。
键(key) → 哈希函数 → 索引(index)
"apple" → hash("apple") → 3
"banana" → hash("banana") → 7
2.2 冲突解决
链地址法(Separate Chaining)
class HashTable:
def __init__(self, capacity=16):
self.capacity = capacity
self.size = 0
self.buckets = [[] for _ in range(capacity)]
self.load_factor_threshold = 0.75
def _hash(self, key):
return hash(key) % self.capacity
def put(self, key, value):
idx = self._hash(key)
bucket = self.buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value) # 更新
return
bucket.append((key, value))
self.size += 1
if self.size / self.capacity > self.load_factor_threshold:
self._resize()
def get(self, key):
idx = self._hash(key)
for k, v in self.buckets[idx]:
if k == key:
return v
raise KeyError(key)
def delete(self, key):
idx = self._hash(key)
bucket = self.buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
self.size -= 1
return v
raise KeyError(key)
def _resize(self):
old_buckets = self.buckets
self.capacity *= 2
self.size = 0
self.buckets = [[] for _ in range(self.capacity)]
for bucket in old_buckets:
for k, v in bucket:
self.put(k, v)
开放寻址法(Open Addressing)
class OpenAddressingHash:
"""线性探测"""
def __init__(self, capacity=16):
self.capacity = capacity
self.size = 0
self.keys = [None] * capacity
self.values = [None] * capacity
self.DELETED = object() # 哨兵标记
def _probe(self, key, i):
return (hash(key) + i) % self.capacity
def put(self, key, value):
for i in range(self.capacity):
idx = self._probe(key, i)
if self.keys[idx] in (None, self.DELETED):
self.keys[idx] = key
self.values[idx] = value
self.size += 1
return
if self.keys[idx] == key:
self.values[idx] = value
return
raise RuntimeError("哈希表已满")
def get(self, key):
for i in range(self.capacity):
idx = self._probe(key, i)
if self.keys[idx] is None:
raise KeyError(key)
if self.keys[idx] == key:
return self.values[idx]
raise KeyError(key)
2.3 哈希函数设计
| 方法 | 说明 | 适用 |
|---|---|---|
| 除留余数法 | hash(key) % m | 通用,m 取质数 |
| 乘法哈希 | floor(m × (A × key mod 1)) | 均匀分布 |
| MurmurHash | 非加密型,速度快、散列性好 | 现代哈希表实现 |
| SHA-256 | 加密型,计算慢 | 安全场景 |
3. 布隆过滤器(Bloom Filter)
3.1 原理
空间效率极高的概率型数据结构,用于判断"元素可能在集合中"或"一定不在集合中"。
位数组 m 个位 + k 个哈希函数
添加元素:用 k 个哈希函数计算 k 个位置,全部置 1
查询元素:k 个位置全为 1 → "可能存在";任一位置为 0 → "一定不存在"
3.2 Python 实现
import hashlib
import math
class BloomFilter:
def __init__(self, expected_items, false_positive_rate=0.01):
"""
expected_items: 预期元素数量
false_positive_rate: 可接受的误判率
"""
self.size = self._optimal_size(expected_items, false_positive_rate)
self.hash_count = self._optimal_hash_count(self.size, expected_items)
self.bit_array = [0] * self.size
self.item_count = 0
def _optimal_size(self, n, p):
return int(-n * math.log(p) / (math.log(2) ** 2))
def _optimal_hash_count(self, m, n):
return int(m / n * math.log(2))
def _hashes(self, item):
"""生成 k 个哈希值(使用双哈希模拟)"""
item = str(item).encode('utf-8')
h1 = int(hashlib.md5(item).hexdigest(), 16)
h2 = int(hashlib.sha256(item).hexdigest(), 16)
for i in range(self.hash_count):
yield (h1 + i * h2) % self.size
def add(self, item):
for idx in self._hashes(item):
self.bit_array[idx] = 1
self.item_count += 1
def __contains__(self, item):
return all(self.bit_array[idx] for idx in self._hashes(item))
# 示例
bf = BloomFilter(expected_items=100000, false_positive_rate=0.01)
for i in range(10000):
bf.add(f"item_{i}")
print("item_5" in bf) # True
print("item_99999" in bf) # False
print("xxx" in bf) # 大概率 False,小概率 True(误判)
3.3 应用场景
| 场景 | 说明 |
|---|---|
| URL 过滤器 | 判断 URL 是否在黑名单中 |
| 缓存穿透防护 | Redis 前判断 key 是否可能有效 |
| 数据库查询优化 | 先过滤肯定不存在的记录 |
| 爬虫去重 | 海量 URL 去重 |
4. 跳表(Skip List)
4.1 原理
概率性平衡的有序数据结构,利用多层索引加速搜索。
Level 3: 1 ---------------------------→ 9
Level 2: 1 -----------→ 5 -----------→ 9
Level 1: 1 ---→ 3 ---→ 5 ---→ 7 ---→ 9
Level 0: → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 (原始有序链表)
搜索 6:
Level 3: 1 → 9 (6 < 9, 下降)
Level 2: 1 → 5 → 9 (6 < 9, 下降)
Level 1: 5 → 7 (6 < 7, 下降)
Level 0: 5 → 6 ✓
Redis 的
ZSet、JavaConcurrentSkipListMap内部使用跳表。
4.2 Python 实现
import random
class SkipListNode:
def __init__(self, val, level):
self.val = val
# forwards[i] 表示第 i 层的下一个节点
self.forwards = [None] * level
class SkipList:
MAX_LEVEL = 16
P = 0.5 # 晋升概率
def __init__(self):
self.head = SkipListNode(None, self.MAX_LEVEL)
self.level = 1
def _random_level(self):
level = 1
while random.random() < self.P and level < self.MAX_LEVEL:
level += 1
return level
def search(self, target):
curr = self.head
for i in range(self.level - 1, -1, -1):
while curr.forwards[i] and curr.forwards[i].val < target:
curr = curr.forwards[i]
curr = curr.forwards[0]
return curr is not None and curr.val == target
def insert(self, val):
update = [None] * self.MAX_LEVEL
curr = self.head
for i in range(self.level - 1, -1, -1):
while curr.forwards[i] and curr.forwards[i].val < val:
curr = curr.forwards[i]
update[i] = curr
new_level = self._random_level()
if new_level > self.level:
for i in range(self.level, new_level):
update[i] = self.head
self.level = new_level
new_node = SkipListNode(val, new_level)
for i in range(new_level):
new_node.forwards[i] = update[i].forwards[i]
update[i].forwards[i] = new_node
def delete(self, val):
update = [None] * self.MAX_LEVEL
curr = self.head
for i in range(self.level - 1, -1, -1):
while curr.forwards[i] and curr.forwards[i].val < val:
curr = curr.forwards[i]
update[i] = curr
target = curr.forwards[0]
if not target or target.val != val:
return False
for i in range(self.level):
if update[i].forwards[i] != target:
break
update[i].forwards[i] = target.forwards[i]
while self.level > 1 and not self.head.forwards[self.level - 1]:
self.level -= 1
return True
4.3 跳表 vs 平衡树 vs 哈希表
| 特性 | 跳表 | 红黑树 | 哈希表 |
|---|---|---|---|
| 有序性 | ✅ | ✅ | ❌ |
| 范围查询 | ✅ O(log n + k) | ✅ O(log n + k) | ❌ |
| 实现复杂度 | 简单 | 复杂 | 中等 |
| 并发友好度 | 高(锁粒度小) | 低 | 中等 |
| 空间占用 | 中等(~1.33n) | 中等 | 高(预留空间) |
6. 一致性哈希与分布式系统
6.1 传统哈希的问题
# 经典哈希:server = hash(key) % N
# 当服务器数量 N 变化时,几乎全部 key 都会重新映射
# 原来在 server0 的 key,加一台机器后可能跑到 server1/server2
6.2 一致性哈希原理
将服务器和 key 映射到同一个环形哈希空间(0 ~ 2³²-1),每个 key 顺时针找到最近的 server:
0 2^32-1
| |
v v
+--- serverA --+
| |
key1 → 顺时针方向 → serverB
| |
+--- serverB --+
| |
+--- serverC --+
虚拟节点:每个物理节点对应多个虚拟节点,解决数据倾斜问题。
import hashlib
import bisect
class ConsistentHash:
def __init__(self, replicas=150):
self.replicas = replicas # 每个物理节点虚拟节点数
self.ring = [] # 排序后的哈希环
self.nodes = {} # hash → 物理节点
self.sorted_keys = []
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.replicas):
key = f"{node}#{i}"
h = self._hash(key)
self.nodes[h] = node
bisect.insort(self.sorted_keys, h)
def remove_node(self, node):
for i in range(self.replicas):
h = self._hash(f"{node}#{i}")
self.sorted_keys.remove(h)
del self.nodes[h]
def get_node(self, key):
if not self.sorted_keys:
return None
h = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, h)
if idx == len(self.sorted_keys):
idx = 0
return self.nodes[self.sorted_keys[idx]]
# 使用
ch = ConsistentHash(replicas=150)
ch.add_node("server1")
ch.add_node("server2")
ch.add_node("server3")
# 查看 key 被路由到哪个节点
print(ch.get_node("user:10001")) # server2
print(ch.get_node("user:10002")) # server1
| 场景 | 变化影响 |
|---|---|
| 增加 1 台服务器 (N→N+1) | 约 1/(N+1) 的数据需要迁移 |
| 减少 1 台服务器 (N→N-1) | 约 1/N 的数据需要迁移 |
| 传统哈希 N→N+1 | 几乎全部数据需要迁移 |
应用场景:分布式缓存分片(Redis Cluster)、分布式存储、负载均衡。
7. 搜索算法进阶
7.1 插值搜索
对均匀分布的有序数组,插值搜索利用值的分布预估位置,平均时间接近 O(log log n):
def interpolation_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right and arr[left] <= target <= arr[right]:
# 根据目标值在范围内的比例估算位置
pos = left + int((right - left) * (target - arr[left]) / (arr[right] - arr[left]))
if arr[pos] == target:
return pos
elif arr[pos] < target:
left = pos + 1
else:
right = pos - 1
return -1
7.2 指数搜索(无界数组搜索)
当数组长度未知时,先以指数步长确定范围,再二分搜索:
def exponential_search(arr, target):
bound = 1
while bound < len(arr) and arr[bound] < target:
bound *= 2
# 在 [bound/2, min(bound, len-1)] 范围内二分
left, right = bound // 2, min(bound, len(arr) - 1)
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
数据结构选择总结
查找场景:
有序数据 + 频繁查找 → 二分查找 / 跳表
无序数据 + 单次查找 → 哈希表
海量数据 + 去重判断 → 布隆过滤器(允许误判)
有序数据 + 范围查询 → 跳表 / B+ 树
参考文章
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。