哈希表扩容怎么处理新插入的值?Swift 是怎么做的?
哈希表是一种非常高效的数据结构,常用于实现 Dictionary
(字典)或者 Set
(集合)等功能。它的最大优点就是查找、插入和删除操作都非常快,大多数情况下时间复杂度可以做到 O(1)。
不过,随着存入的数据越来越多,哈希表也可能遇到“哈希冲突”越来越严重的问题。这时候,就需要“扩容”了,也就是创建一个更大的哈希表,把旧数据迁移过去。
很多人可能会疑惑:那如果扩容的时候,还有新的数据要插入怎么办?
别急,我们这就来详细讲讲这件事,顺便看看 Swift 是怎么做的。
哈希表里的“桶”是什么?
在讲扩容前,先搞清楚一个关键概念:桶(bucket)。
哈希表的底层可以理解为一个“数组”,数组里的每一格我们就叫它一个桶。当我们往哈希表插入一个值时,系统会根据这个值的 key 算出一个哈希值,然后再根据哈希值决定把它放进哪一个桶里。
比如你有一个容量为 8 的哈希表,key 的哈希值是 17,那么 17 % 8 = 1,就把数据放到编号为 1 的桶里。
为什么要扩容?
如果桶的数量太少,而数据太多,很多值就会被分配到同一个桶里,形成“哈希冲突”。这样每次查找的时候,可能就得在这个桶里的所有值里慢慢找,效率就下来了。
为了避免这种情况,一般会在负载因子(数据量 / 桶的数量)超过某个阈值(通常是 0.75)时触发扩容。
扩容的时候怎么插入新值?3 种常见方法
扩容是必须的,但问题来了:如果在扩容的过程中,又有新的值要插入怎么办?
这里有三种常见的处理方式:
方法一:等扩容完再插入
这就像是“暂停营业”——扩容期间先不接新单,新值暂时放一边,等扩容完成后再插入。
优点:简单,容易实现。
缺点:用户插入数据要等扩容完成,响应时间变长,影响体验。
方法二:扩容和插入同时进行(并发)
这个思路是让插入操作照常进行,扩容也在后台同步进行。
怎么处理新值?
-
如果新值对应的桶已经迁移到新哈希表,就直接插入新表;
-
如果还没迁移,就暂时插在旧表里,等那个桶被迁移的时候一并带走。
优点:不用等,性能更好。
缺点:实现难度大,要确保线程安全,数据一致性更难处理。
方法三:渐进式扩容(Swift 采用的)
这是 Swift 使用的策略,也是很多现代语言的做法。
简单说:扩容这件事不一次干完,而是“分几次慢慢做”。
具体流程是这样:
-
发现需要扩容后,创建一个容量更大的新表(一般是原来的 2 倍);
-
不立刻迁移所有数据,而是每次插入、删除、查找时,顺手迁移一部分旧数据;
-
随着越来越多的操作,旧表里的桶就会逐步搬到新表里;
-
最后旧表搬空,就正式使用新表。
优点:
-
每次操作的开销小;
-
不会因为一次扩容造成“卡顿”;
-
插入、删除、查找照常进行,用户感知不到扩容过程。
缺点:
-
实现上要维护两张表,稍复杂一些;
-
扩容完成需要更多操作次数。
看个例子:Python 版渐进式扩容(简化版)
class HashTable:def __init__(self, capacity=8):self.capacity = capacityself.size = 0self.buckets = [[] for _ in range(capacity)]self.is_resizing = Falseself.new_buckets = Noneself.next_index = 0def insert(self, key, value):if not self.is_resizing and self.size / self.capacity > 0.75:self.start_resize()if self.is_resizing:self.migrate_step()hash_idx = hash(key) % self.capacityif self.is_resizing and hash_idx < self.next_index:new_hash_idx = hash(key) % (self.capacity * 2)self.new_buckets[new_hash_idx].append((key, value))else:self.buckets[hash_idx].append((key, value))self.size += 1def start_resize(self):self.is_resizing = Trueself.new_buckets = [[] for _ in range(self.capacity * 2)]self.next_index = 0def migrate_step(self):if self.next_index < self.capacity:bucket = self.buckets[self.next_index]for key, value in bucket:new_hash_idx = hash(key) % (self.capacity * 2)self.new_buckets[new_hash_idx].append((key, value))self.next_index += 1if self.next_index == self.capacity:self.buckets = self.new_bucketsself.capacity *= 2self.is_resizing = Falseself.new_buckets = Noneself.next_index = 0
Swift 的实现也是渐进式扩容!
Swift 中的 Dictionary
和 Set
类型就是基于哈希表的,它们采用的就是渐进式扩容策略。
从 Swift 的源码中也可以看出它的逻辑:
internal mutating func ensureUniqueCapacity(forGrowth: Bool) {if forGrowth, loadFactor > HashTable.maximumLoadFactor {rehash(using: HashTable(capacity: capacityAfterGrowth))}
}internal mutating func rehash(using newTable: HashTable) {let oldTable = _variant.table_variant = .table(newTable)_incrementalRehashState = IncrementalRehashState(oldTable: oldTable,oldCapacity: oldTable.capacity)
}
可以看到:
-
扩容时不会立即搬迁所有数据;
-
后续的插入、删除操作会触发逐步搬迁;
-
Swift 内部维护了“双表结构”和迁移状态。
总结:Swift 为什么选择渐进式扩容?
优点明显:
- 插入速度不受扩容影响
- 没有卡顿或阻塞用户操作
- 插入操作的平均耗时依然是 O(1)
所以在实际应用中,像 Swift 这样选用渐进式扩容,可以让哈希表性能更稳定,同时也更适合在多线程或并发场景中使用。