当前位置: 首页 > news >正文

哈希表扩容怎么处理新插入的值?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 = 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 这样选用渐进式扩容,可以让哈希表性能更稳定,同时也更适合在多线程或并发场景中使用。

http://www.xdnf.cn/news/363709.html

相关文章:

  • 力扣-19.删除链表的倒数第N个结点
  • Nacos源码—Nacos配置中心实现分析
  • Mysql数据库进阶
  • LMMSE、MMSE和LS
  • vscode 配置doxygen注释和snippet
  • RT-Thread 深入系列 Part 1:RT-Thread 全景总览
  • 【赛元8523触摸按键开发调试】
  • 【vue3】vue3中封装懒加载指令
  • C++ Lambda表达式详解:匿名函数的艺术与现代编程实践
  • 数字经济时代下的消费行为变迁与经济学启示
  • 解决 Redis 缓存与数据库一致性问题的技术指南
  • 【Linux网络】Socket-TCP相关函数
  • 大模型提示词策略
  • 赋能智能交通:时空图卷积网络引领速度预测新变革
  • PostgreSQL技术大讲堂 - 第89讲:重讲数据库完全恢复
  • 图解gpt之Seq2Seq架构与序列到序列模型
  • 【某OTA网站】phantom-token 1004
  • vue 监听元素大小变化 element-resize-detector
  • 《Vuejs与实现》第 6 章(原始值响应式方案)
  • 蓝桥杯青少 图形化编程(Scratch)编程题每日一练——图形特效
  • 嵌套路由~
  • leetcode - 双指针问题
  • Linux C语言线程编程入门笔记
  • uni-app 中的条件编译与跨端兼容
  • 区块链详解
  • 独立自主的网络浏览器——Ladybird
  • 类加载器, JVM类加载机制
  • 【PostgreSQL 中插入数据时跳过已存在记录的方法】
  • 阿里云服务器数据库故障排查指南?
  • springboot 加载 tomcat 源码追踪