原文地址:https://blog.csdn.net/y1196645376/article/details/94348873
1.table的特性
1. 在Lua中table是個非常重要的類型,通過使用table的一些特性可以實現許多數據結構,例如map,array queue,stack等。
2. 通過使用者角度來講,table既可以當作array使用也可以當作map使用,那麼對於設計者來講,那麼需要保證table的高效率的查找、插入、遍歷。
3. 當然,table的設計者還提出了metatable(元表)的概念,以供使用者可以用來實現繼承、操作符重載等設計,不過metatable暫時不在這邊文章進行討論。
2.table的定義
typedef union TKey {
struct {
TValuefields;
int next; /* 用於標記鏈表下一個節點 */
} nk;
TValue tvk;
} TKey;
typedef struct Node {
TValue i_val;
TKey i_key;
} Node;
typedef struct Table {
CommonHeader;
lu_byte flags; /* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode; /* log2 of size of 'node' array */
unsigned int sizearray; /* size of 'array' array */
TValue *array; /* array part */
Node *node;
Node *lastfree; /* any free position is before this position */
struct Table *metatable;
GCObject *gclist;
} Table;
- flags : 元方法的標記,用於查詢table是否包含某個類別的元方法
- lsizenode : (1<<lsizenode)表示table的hash部分大小
- sizearray : table的數組部分大小
- array : table的array數組首節點
- node :table的hash表首節點
- lastfree : 表示table的hash表空閒節點的遊標
- metatable : 元表
- gclist : table gc相關的參數
爲了提高table的插入查找效率,在table的設計上,採用了array數組和hashtable(哈希表)兩種數據的結合。
所以table會將部分整形key作爲下標放在數組中, 其餘的整形key和其他類型的key都放在hash表中。
3.hash表結構
在table中的實現中,hash表佔絕大部分比重,下面是table中hash表的結構示意簡圖:
hash表在解決衝突有兩個常用的方法:
1. 開放定址法:當衝突發生時,使用某種探查(亦稱探測)技術在散列表中形成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定 的關鍵字,或者碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的 地址則表明表中無待查的關鍵字,即查找失敗。
2. 鏈地址法:又叫拉鍊法,所有關鍵字爲同義詞的結點鏈接在同一個單鏈表中。若選定的散列表長度爲m,則可將散列表定義爲一個由m個頭指針組成的指針數 組T[0…m-1]。凡是散列地址爲i的結點,均插入到以T[i]爲頭指針的單鏈表中。T中各分量的初值均應爲空指針。在拉鍊法中,裝填因子α可以大於 1,但一般均取α≤1。
簡單對比可以發現以上兩種方法的優缺點:
開放定址法相比鏈地址法節省更多的內存,但在插入和查找的時候擁有更高的複雜度。
但是table中的hash表的實現結合了以上兩種方法的一些特性:
查找和插入等同鏈地址法複雜度。
內存開銷近似等同於開放定址法。
原因是table中的hash表雖然採用鏈地址法的形式處理衝突,但是鏈表中的額外的節點是hash表中的節點,並不需要額外開闢鏈表節點;下面是TKey結構的介紹:
那麼如何將hash表的空餘節點利用起來作爲鏈表的節點呢?這個算法的實現得益於lastfree這個指針的作用,後面會詳細介紹。
4.table的創建
lua中通過luaH_new來創建一個新table:
Table *luaH_new (lua_State *L) {
GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table));
Table *t = gco2t(o);
t->metatable = NULL;
t->flags = cast_byte(~0);
t->array = NULL;
t->sizearray = 0;
setnodevector(L, t, 0);
return t;
}
此時,table中的array部分和hash部分都爲空。
5.luaH_get分析
table中通過這個函數來從表中查找key對應的值;可以看到它會通過key的類型不同,從而進行不同的處理。
const TValue *luaH_get (Table *t, const TValue *key) {
switch (ttype(key)) {
case LUA_TSHRSTR: return luaH_getshortstr(t, tsvalue(key));
case LUA_TNUMINT: return luaH_getint(t, ivalue(key));
case LUA_TNIL: return luaO_nilobject;
case LUA_TNUMFLT: {
lua_Integer k;
if (luaV_tointeger(key, &k, 0)) /* index is int? */
return luaH_getint(t, k); /* use specialized version */
/* else... */
} /* FALLTHROUGH */
default:
return getgeneric(t, key);
}
}
- 如果key是nil,則直接返回nil。
- 如果key是整數類形,則調用luaH_getint來處理,因爲整形key可能會在array中取值。
- 如果key是浮點數類型,則首先判斷key是否能轉化爲整數,如果是則調用luaH_getint,否則調用getgeneric。
- 如果key是短字符串類型,則調用luaH_getshortstr來處理。(其實這個case有點不理解,短字符串也可以交給getgeneric來處理)
- 其他類型的key,都使用getgeneric來處理。
下面着重分析luaH_getint、getgeneric這兩個函數的流程。
const TValue *luaH_getint (Table *t, lua_Integer key) {
/* (1 <= key && key <= t->sizearray) */
if (l_castS2U(key) - 1 < t->sizearray)
return &t->array[key - 1];
else {
Node *n = hashint(t, key);
for (;;) { /* check whether 'key' is somewhere in the chain */
if (ttisinteger(gkey(n)) && ivalue(gkey(n)) == key)
return gval(n); /* that's it */
else {
int nx = gnext(n);
if (nx == 0) break;
n += nx;
}
}
return luaO_nilobject;
}
}
前面說過,table由array和hashtable組成,所以對於整形的key需要先去數組範圍內找:
- 如果key的大小在數組大小範圍內,那麼就直接在數組中查找值並返回。
- 否則,獲取int的hash值對應的hashslot,然後在slot-link上找到key對應的值並返回。(和鏈地址法的查找是一樣的)
- 如果找不到,則返回nil。
static const TValue *getgeneric (Table *t, const TValue *key) {
Node *n = mainposition(t, key);
for (;;) { /* check whether 'key' is somewhere in the chain */
if (luaV_rawequalobj(gkey(n), key))
return gval(n); /* that's it */
else {
int nx = gnext(n);
if (nx == 0)
return luaO_nilobject; /* not found */
n += nx;
}
}
}
其實getgeneric流程就是傳統的鏈地址法查找流程,不過值得注意的是mainposition函數,在這裏面區分了lua對於各種類型的hash方式:
static Node *mainposition (const Table *t, const TValue *key) {
switch (ttype(key)) {
case LUA_TNUMINT:
return hashint(t, ivalue(key));
case LUA_TNUMFLT:
return hashmod(t, l_hashfloat(fltvalue(key)));
case LUA_TSHRSTR:
return hashstr(t, tsvalue(key));
case LUA_TLNGSTR:
return hashpow2(t, luaS_hashlongstr(tsvalue(key)));
case LUA_TBOOLEAN:
return hashboolean(t, bvalue(key));
case LUA_TLIGHTUSERDATA:
return hashpointer(t, pvalue(key));
case LUA_TLCF:
return hashpointer(t, fvalue(key));
default:
lua_assert(!ttisdeadkey(key));
return hashpointer(t, gcvalue(key));
}
}
mainpostion爲hash值%hash表的大小。
值得注意的是對於字符串的hash處理,lua區分了長字符串和短字符串(5.3之後對字符串按照長短做了區分處理)
- 對於短字符串,lua都存放在stringtable中,所以對於短字符串只有一個實體。可以直接使用string在stringtable中的hash值。
- 對於長字符串,lua中可能會存在多個實例,所以需要通過luaS_hash來計算其hash值。
6.luaH_set分析
TValue *luaH_set (lua_State *L, Table *t, const TValue *key) {
const TValue *p = luaH_get(t, key);
if (p != luaO_nilobject)
return cast(TValue *, p);
else return luaH_newkey(L, t, key);
}
luaH_set 不是傳統意義上的set,也就是直接傳入key和value然後設置,而是傳入key會返回這個key對應的TValue,然後再通過setobj2t對這個TValue進行設置。所以這個set函數就很簡單了:
- 首先調用luaH_get查找table是否已經存在這個key了,有則直接返回。
- 否則調用luaH_newkey創建key,並返回對應的TValue。(注意此時key一定不在數組部分內)
那麼luaH_set的分析就轉化爲luaH_newkey的分析:
看流程圖有點複雜,下面給出實際的例子來舉例(先不考慮Rehash部分):
//假設tb的hash表默認大小爲8個元素
local tb = {}
tb[3] = 'a'
tb[11] = 'b'
tb[19] = 'c'
tb[6] = 'd'
tb[14] = 'e'
1. 執行完local tb = {}之後,tb中的hash表狀態是這樣:
空餘節點指針lastfree指向了最後一個node。
注意:table創建默認hash表大小爲0,這裏爲了方便描述假設初始大小爲8,這樣就不用管rehash部分了
2. 執行完tb[3] = 'a’之後,tb中的hash表狀態是這樣:
因爲3的mainposition爲3,所以放在了n3位置。
注意:key的manposition等於hash(k)%table_size, 所以mainposition(3)=3%8
3. 執行完tb[11] = 'b’之後,tb中的hash表狀態是這樣:
因爲11的mainposition也爲3,然而3位置已經被佔用了,所以此時使用lastfree獲取一個空節點n7,將當前key存儲在n7位置上,並且使用頭插法將n7節點插入在mainposition節點n3之後,所以這裏的next = n7 - n3 = 4。
4. 執行完tb[19] = 'c’之後,tb中的hash表狀態是這樣:
原因和上面類似,只不過注意的是,19是插入在mainposition節點n3和mp的next節點n7之間,所以需要重新維護n3的next值。
5. 執行完tb[6] = 'd’之後,tb中的hash表狀態是這樣:
在這一步中有些不一樣的處理,首先還是算出6的mainposition爲6,然後發現n6已經被key:19佔用了。但是此時我們不能直接使用lastfree來存儲key:6,因爲19和6不是同一個鏈表上的,也就是說key:19搶了key:6的位置:
mainpositon(19) = 19 % 8 = 3 mainpositon(6) = 6 % 8 = 6
對於這種情況,我們需要key:19讓出位置,通過lastfree申請一個空節點n5,然後將19的位置換到n5上(注意維護next節點)。然後將key:6放在n6節點上。
這部分操作就是流程圖上Resetpos部分
6. 執行完tb[14] = 'e’之後,tb中的hash表狀態是這樣:
這裏和步驟3類似。
7. 下面再來分析一下Rehash的部分
假如此時table裏面的array和hash表狀態如下:
此時再插入一個key:10,因爲lastfree已經無法獲取空節點了,所以觸發了rehash。
- 首先通過numusearray計算數組部分val不爲nil的所有整形數目,和nums[]。對於nums[i] = j,其意義表示key在2^i-1 到2^i之間其整形key的個數有j個。
- 然後再通過numusehash計算hash部分Val不爲nil的所有整形數目,和nums[]。
- 通過整形key的個數確定array的大小,computesizes,這裏確定array的大小有個規則就是要滿足2的冪size,並且整形key數目num > arraySize / 2,還要保證放入整形key的數目高於arraySize / 2。
- 最後根據array大小和總key個數,確定hash表的大小。(ps:hash表的大小也只能是2的冪,如果不是則向上對齊)
通過上面的規則可以計算得到array部分的大小爲4,hash表大小爲7 - 3 = 4。(7是指Key的總數,3是指能放入數組的Key的個數)
無論是array的rehash還是hash表的rehash都是先開闢新的內存,然後將原來的元素重新插入。
插入key:10後的狀態爲:
值得注意的是:table元素的刪除是通過table[key] = nil來實現的,然而通過我們上面對luaH_set介紹我們可以知道,僅僅是把把key對應的val設置爲nil而已,並沒有真正的從table中刪除這個key,只有當插入新的key或者rehash的時候纔可能會被覆蓋或者清除。
8.luaH_getn分析
lua中獲取一個table的長度一般是使用#操作符,不過這裏並不是直接取的table的array部分的長度,而是說table整數key索引序列連續長度。也就是說如果我們把tale當做一個數組在使用的時候返回的就是數組的長度。例如:
在上圖所示table中一共有1,2,3,‘a’,‘b’,'c’六個key,雖然數組部分只有1,2兩個,但是對table取長度爲3,因爲整數key連續長度爲1,2,3。
值得注意的是,假如在整數key序列中間出現了中斷,那麼取出來的長度是“不確定的”
如上兩圖table1和table2,table的array部分和hash部分都是整數key。雖然索引序列都在3處出現了中斷,但是分別對table1和table2取長度值分別是6和2。按道理來說兩個table的長度都應該是2,但是爲什麼會出現一個6這個奇怪的“錯誤值”呢?
根本原因是table對於取其長度的算法導致的,而luaH_getn就是用來求table的長度值的函數。下面來看下這個函數源碼:
int luaH_getn (Table *t) {
unsigned int j = t->sizearray;
if (j > 0 && ttisnil(&t->array[j - 1])) {
/* there is a boundary in the array part: (binary) search for it */
unsigned int i = 0;
while (j - i > 1) {
unsigned int m = (i+j)/2;
if (ttisnil(&t->array[m - 1])) j = m;
else i = m;
}
return i;
}
/* else must find a boundary in hash part */
else if (isdummy(t)) /* hash part is empty? */
return j; /* that is easy... */
else return unbound_search(t, j);
}
static int unbound_search (Table *t, unsigned int j) {
unsigned int i = j; /* i is zero or a present index */
j++;
/* find 'i' and 'j' such that i is present and j is not */
while (!ttisnil(luaH_getint(t, j))) {
i = j;
if (j > cast(unsigned int, MAX_INT)/2) { /* overflow? */
/* table was built with bad purposes: resort to linear search */
i = 1;
while (!ttisnil(luaH_getint(t, i))) i++;
return i - 1;
}
j *= 2;
}
/* now do a binary search between them */
while (j - i > 1) {
unsigned int m = (i+j)/2;
if (ttisnil(luaH_getint(t, m))) j = m;
else i = m;
}
return i;
}
lua在取table其長度值,實際上在找一個整形key,滿足
- table[key] != nil
- table[key+1] == nil
一旦找到一個這樣的key,那麼這個key就會被認定爲table的長度。
爲了提高查找效率,lua源碼並沒有進行遍歷查找,而是通過二分查找。(時間複雜度從O(n)降到O(logn))
具體算法流程如下:
- 如果table數組部分的最後一個元素爲nil,那麼將在數組部分進行二分查找
- 如果table數組部分的最後一個元素不爲nil,那麼將在hash部分進行二分查找
由此可見,上面兩個例子中出現差異的問題就是,table[4]作爲數組部分的最後一個元素導致兩個table分別在數組部分和hash部分查找key導致的。