哈希表扩容怎么处理新插入的值?Swift 是怎么做的?

哈希表是一种非常高效的数据结构,常用于实现 Dictionary(字典)或者 Set(集合)等功能。它的最大优点就是查找、插入和删除操作都非常快,大多数情况下时间复杂度可以做到 O(1)。

不过,随着存入的数据越来越多,哈希表也可能遇到“哈希冲突”越来越严重的问题。这时候,就需要“扩容”了,也就是创建一个更大的哈希表,把旧数据迁移过去。

很多人可能会疑惑:那如果扩容的时候,还有新的数据要插入怎么办?

别急,我们这就来详细讲讲这件事,顺便看看 Swift 是怎么做的。


哈希表里的“桶”是什么?

在讲扩容前,先搞清楚一个关键概念:桶(bucket)

哈希表的底层可以理解为一个“数组”,数组里的每一格我们就叫它一个。当我们往哈希表插入一个值时,系统会根据这个值的 key 算出一个哈希值,然后再根据哈希值决定把它放进哪一个桶里。

比如你有一个容量为 8 的哈希表,key 的哈希值是 17,那么 17 % 8 = 1,就把数据放到编号为 1 的桶里。


为什么要扩容?

如果桶的数量太少,而数据太多,很多值就会被分配到同一个桶里,形成“哈希冲突”。这样每次查找的时候,可能就得在这个桶里的所有值里慢慢找,效率就下来了。

为了避免这种情况,一般会在负载因子(数据量 / 桶的数量)超过某个阈值(通常是 0.75)时触发扩容。


扩容的时候怎么插入新值?3 种常见方法

扩容是必须的,但问题来了:如果在扩容的过程中,又有新的值要插入怎么办?

这里有三种常见的处理方式:


方法一:等扩容完再插入

这就像是“暂停营业”——扩容期间先不接新单,新值暂时放一边,等扩容完成后再插入。

优点:简单,容易实现。

缺点:用户插入数据要等扩容完成,响应时间变长,影响体验。


方法二:扩容和插入同时进行(并发)

这个思路是让插入操作照常进行,扩容也在后台同步进行。

怎么处理新值?

  • 如果新值对应的桶已经迁移到新哈希表,就直接插入新表;

  • 如果还没迁移,就暂时插在旧表里,等那个桶被迁移的时候一并带走。

优点:不用等,性能更好。

缺点:实现难度大,要确保线程安全,数据一致性更难处理。


方法三:渐进式扩容(Swift 采用的)

这是 Swift 使用的策略,也是很多现代语言的做法。

简单说:扩容这件事不一次干完,而是“分几次慢慢做”

具体流程是这样:

  1. 发现需要扩容后,创建一个容量更大的新表(一般是原来的 2 倍);

  2. 不立刻迁移所有数据,而是每次插入、删除、查找时,顺手迁移一部分旧数据

  3. 随着越来越多的操作,旧表里的桶就会逐步搬到新表里;

  4. 最后旧表搬空,就正式使用新表。

优点

  • 每次操作的开销小;

  • 不会因为一次扩容造成“卡顿”;

  • 插入、删除、查找照常进行,用户感知不到扩容过程。

缺点

  • 实现上要维护两张表,稍复杂一些;

  • 扩容完成需要更多操作次数。


看个例子:Python 版渐进式扩容(简化版)

class HashTable:
    def __init__(self, capacity=8):
        self.capacity = capacity
        self.size = 0
        self.buckets = [[] for _ in range(capacity)]
        self.is_resizing = False
        self.new_buckets = None
        self.next_index = 0

    def 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.capacity
        if 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 += 1

    def start_resize(self):
        self.is_resizing = True
        self.new_buckets = [[] for _ in range(self.capacity * 2)]
        self.next_index = 0

    def 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 += 1

            if self.next_index == self.capacity:
                self.buckets = self.new_buckets
                self.capacity *= 2
                self.is_resizing = False
                self.new_buckets = None
                self.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 这样选用渐进式扩容,可以让哈希表性能更稳定,同时也更适合在多线程或并发场景中使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值