Lua hash
一般哈希表处理冲突有两种方式,拉链法和开放地址法。拉链法就是哈希表的每个元素都是一个链表,如果有冲突的键就放在链表里面。而开放地址法是如果遇到了冲突,就在计算一个哈希值,直到没有冲突位置。
拉链法的优点就是实现简单,缺点也是有的:链表会导致低的缓存命中率,并且分配链表节点本身也会对内存分配器产生压力(主要是大量小块内存分配会导致碎片),而且有个更严重的问题:不太好估计哈希表的装载因子,因此不太容易判断啥时候需要扩充哈希表。
而开放地址法避免了这些问题,首先因为所有节点都存在哈希表里面,因此很容易就能估计装载因子,其次也不会有内存分配的问题,避免了零散碎片或者实现内存池的必要。不过问题是要找到一个新的地址就需要重新计算哈希,是一个负担,而且如果在对应哈希查不到元素也可能并不是没有元素,而只是之前的冲突导致元素不再它的”主位置”,这样就需要更多复杂的判断。
解决主位置的问题其实很简单:可以设置一个”墓碑”,删除的时候并不是直接删除,而是设置一个”墓碑”当发现是墓碑的时候,证明该位置曾经是有元素的,这时按照冲突的方式继续查找。总的来说,如果不考虑装载因子,因为要重复搜索哈希表,开放地址法的查找是会比较慢的。
Lua是将拉链法和开放地址法结合在了一起。具体的做法是这样的:Lua的哈希表主体是开放地址法,即所有元素都被存放在表中,而不是在链表里。但是,每个元素也的确有一个链表节点。在开始的时候,Lua维护一个”空闲槽指针”这个指针之后的位置一定都是有元素的。当发现冲突的时候,会将空闲槽指针迁移,以找到一个空闲的槽位,然后元素会被放在这个槽位里,并在主位置通过链表的方式链接进来!即对插入而言,Lua的哈希表采用实际上是不计算哈希的开放地址法(随意找一个槽位),而对于查找而言,因为有链表节点的存在,是按照拉链法的方式进行查找的。这种方法利用了两者的优点,又规避了缺陷。
这里面其实还是有一些其它可以优化的地方。比如墓碑还是需要的,这用于在主位置找不到以后,得得到后续节点的一个索引。但是通过在插入/删除节点时,挪动后续的节点的方法,就可以省略掉墓碑和对应的遍历操作。另外这种哈希表判断装载因子的方式也很简单—只要空闲槽指针指向表的第一个元素,就意味着表已经满了,这时就需要rehash了。
Lua的哈希函数使用的是通常的DJB法,也叫times33,计算简单,不过可能效果不太好,对随机的字符这种方式产生的哈希是比较平均的。