Table内部实现2
这一节介绍Lua唯一的数据结构table,相对于大部分语言提供数组和字典两种类型,Lua将其合二为一,颇为精巧的实现了table。
table充分体现了Lua语言的特点,用最简练的语法表达丰富的信息,但也增加了用户的理解成本。table包含数组和哈希两部分功能,所以实现起来颇为复杂。
本文展示的代码来自llamavm,并非Lua源码,C++版本的实现比较容易理解。
实现部分包括:
数据存储
获取key值
修改key值
自动扩容
计算数组长度
遍历table
示例代码:
执行结果:
Part1 数据存储
若将数组下标索引(1到n)作为整数key,用一个哈希表就能实现table。
key可以为nil之外的任意类型,比如整数key、字符串key,布尔key,甚至可以用函数、table作为key。
在Lua5.0之前,table内部用一个哈希表实现,5.0版本后拆分为数组和哈希两个部分。
两种实现的区别
(1)一个哈希表实现,所有key都存储在哈希表内
(2)数组加哈希表实现,部分整数key存放在数组,其余key存放在哈希表
将部分整数key放在数组部分,显然是为了性能考虑,这里引用一段官方说明:
混合机制有两个优点:
第一:访问整型key的操作会变得更快了,因为不再需要哈希。
第二:更重要的是,数组部分只占原来哈希部分的一半大小,因为哈希部分需要同时存储key和value,而数组部分的key已经隐含在下标了。
结果是,如果一个table是作为数组使用的,它的表现就像数组一样,只要它的整型key是密集分布的。而且,哈希部分没有内存或者时间的代价,因为作为数组使用时,哈希部分不存在。
反过来说,如果table是作为记录使用而非数组,那么数组部分就是空的。这些节省下来的内存是重要的,因为对于Lua程序来说,创建大量小table是很常见的(比如用table来表示object)。
Lua的table也能优雅的处理稀疏数组:语句a={[1000000000]=1}在哈希部分创建了一个键值对,而非一个10亿元素的数组。
数组部分的实现比较简单,主要介绍哈希部分的实现。
在《字符串实现》一节里介绍了通过哈希表实现字符串池,采用链地址法解决hash冲突。在table里采用开放地址法解决hash冲突,table类型定义如下:
数组部分存放在arrayData,每个元素为一个Object
哈希部分存放在hashData,每个元素为一个Node
Node包括key、value,以及指向下一个冲突结点的指针
last_free表示最后一个空闲结点的位置,避免遍历查找
举例说明
(1)有3个结点,key分别为aa、bb、cc
{'aa', 100}
{'bb', 200}
{'cc', 300}
其中'aa'和'cc'的hash值都为401,产生冲突(哈希算法比较差),'bb'的哈希码为402
hashcode('aa') = 401
hashcode('bb') = 402
hashcode('cc') = 401
(2)添加这3个结点到table,Node数组大小为4,根据key的hash值计算数组位置
pos_aa = hashcode('aa') % 4 = 1
pos_bb = hashcode('bb') % 4 = 2
pos_cc = hashcode('cc') % 4 = 1
(3)添加'aa',添加到位置1
hashData[1] = Node
(4)添加'bb',添加到位置2
hashData[2] = Node
(5)添加'cc',位置1已经被'aa'占据,挑选一个空闲位置,last_free=3,添加到位置3
hashData[3] = Node
3个结点添加完毕,Node数组为:
hashData[0] = Node {}
hashData[1] = Node
hashData[2] = Node
hashData[3] = Node
(6)由于'aa'和'cc'冲突,'cc'不在其主位置上(应该在位置1,实际在位置3),需要将'aa'和'cc'串联起来,构成冲突链
hashData[0] = Node {}
hashData[1] = Node
hashData[2] = Node
hashData[3] = Node
查找'cc'时,先查找位置1,找到'aa',再根据'aa'的next指针找到'cc'。
table结构图:
Part2 获取key值
根据前面的分析,大概了解key的查询方法,流程如下:
先确定key是否在数组部分,比如数组部分长度为4,若key为 1~4 会在数组部分查找,其余key都在哈希部分查找
若在数组部分,直接根据索引查找。数组部分的扩容,在rehash部分介绍
若在哈希部分,先根据key的hash值计算其位置,再通过Node的next指针遍历冲突链,找到对应key。
关键点在于如何计算key的hash值,key可以为多种类型,需要针对每种类型计算其哈希值。
(1)字符串
字符串对象本身携带hash值,可以直接使用。
(2)数值
整数值可以直接用作hash值。对于浮点数,考虑到小数部分的不同也会影响hash值,可以将小数累加到整数部分
n = 123.456789
hashcode(n) =(n - (int)n) * 1000000+ n = 456912
(3)指针类型
指针本身就是数值,可以直接用作hash值
hashcode(key) = (long)ptr
总体来说,应尽量利用数据的每个字节计算hash值,以达到hash散列的效果。
Part3 修改key值
要修改key的值,需要先找到key所在的位置,这一点和“获取key值”原理相同。若找到对应的key,直接修改其value值。
若没找到key,添加到哈希部分,流程如下:
当主位置被占用,且有空闲结点的时候,需要调整结点的位置,流程如下:
这里逻辑有些复杂,举例讲解:
(1)假定Node数组长度为4,先添加key 'aa',通过hash值计算其主位置应该为1
Node[1] = 'aa'
(2)添加key 'bb',恰巧其主位置也为1,由于'aa'在其正确位置上,分配最后一个空闲结点给'bb',且建立冲突链,'aa'指向'bb'
Node[1] = 'aa',next->[3]'bb'
Node[3] = 'bb'
(3)添加key 'cc',其主位置为3,但'bb'已经占用了位置3,由于'bb'的实际主位置应该为1,所以需要将'bb'移走,归还给'cc'
通过冲突链,获取'bb'的前置结点'aa'
分配空闲位置给'bb',last_free=2
'aa'指向'bb'的新位置
'cc'存放在其主位置
Node[1] = 'aa',next->[2]'bb'
Node[2] = 'bb'
Node[3] = 'cc'
'bb'从位置3挪动到位置2,'cc'使用位置3,在挪动前后,'aa'始终指向'bb'。