zoukankan      html  css  js  c++  java
  • lua的table实现以及遍历方式

    最近遇到一件很郁闷的事情,游戏里面我老是当队长!后来发现是因为队伍里每个人的数据会以游戏的ID为key,其余要用到的数据为value,放在一个table里面。然后用pairs遍历这个table,一个个加入队伍,所以谁当队长实际上和pairs的遍历顺序有关,具体来说是和pairs找到的第一个元素有关。一时兴起,重新看了一下lua的table的实现,终于找到原因!
    一如既往,先报源码:
    Ltable.c     table的主要代码在这里
    Lobject.h     lua的基础对象的定义,包括table
    假设我的ID是100005(纯粹是个假设,我当然不可能把我真的ID报出来啦,不过我的ID和这个100005一样苦逼啊有木有)。假设队员的ID是100001,100002,100003,100004,经过试验用pairs来遍历这些key的顺序是100005,100002,100003,100004,100001,下面就看看为什么遍历顺序是这样的。
    先看看在Lobject.h中的关键数据结构:
    /*
    ** Common Header for all collectable objects (in macro form, to be
    ** included in other objects)
    */
    #define CommonHeader     GCObject *next; lu_byte tt; lu_byte marked
    ....
    typedef struct Table {
    CommonHeader;
    lu_byte flags;  /* 1<<p means tagmethod(p) is not present */
    lu_byte lsizenode;  /* log2 of size of `node' array */
    struct Table *metatable;
    TValue *array;  /* array part */
    Node *node;
    Node *lastfree;  /* any free position is before this position */
    GCObject *gclist;
    int sizearray;  /* size of `array' array */
    } Table;
    lua的table实际上是一个数组和hash表的结合体,定义中我们只关心lsizenode,array,node,lastfree和sizearray,其余的部分是每个lua基本类型都有的部分。lsizenode,node,lastfree这三个属性存储的就是哈希表部分;array和sizearray存储的就是数组部分。数组部分比较简单,array就是数组本身,sizearray是数组的大小;哈希表部分的话,node指向哈希表起始地址,lsizenode是log2(node指向的哈希表节点数目),注意2^lsizenode不等于哈希表中存储变量的数目,因为哈希表是有可能有冲突的,所以一个哈希表节点是一个链表的表头,他可能对应多个存储变量。lastfree指向node里面最后一个未用的节点(这个用法很特别,后面会详细说)。
    /*
    ** Tagged Values
    */
    #define TValuefields     Value value; int tt
    ....
    typedef struct lua_TValue {
    TValuefields;
    } TValue;
    ...
    typedef union TKey {
    struct {
    TValuefields;
    struct Node *next;  /* for chaining */
    } nk;
    TValue tvk;
    } TKey;
    typedef struct Node {
    TValue i_val;
    TKey i_key;
    } Node;
    Node是哈希表中节点的结构,i_val是这个节点对应value,i_key是节点对应的key,其类型为TKey。TKey的定义很巧究(也很坑爹),这是一个union,TValue的定义实际上也是TValuefields这个宏,所以TKey的内存分布如下所示。
    |------------------------------|
    |value; |           |            |
    |----------   tvk   |            |
    |tt;       |           |      nk   |
    |-------------------|             |
    |next    |   next |             |
    |-------------------------------|
    画得很丑,将就着看吧。简单来说如果有TKey  a;那么
    a.tvk.value == a.nk.value
    a.tvk.tt == a.nk.tt
    lua之所以用这种方式来表示Node,感觉一个很重要的原因是为了适应不同参数类型的接口。
    TKey中tvk是这个key的值,nk中的next则指向key冲突的下一个节点。lua的hash表的hash算法比较特别,一般的hash表都是根据key算出hash(key),然后把这个key放在hash表的hash(key)位置上,如果有冲突的话,就放在hash(key)位置的链表上。
    但是lua的hash表中,如果有冲突的话,lua会找hash表中一个空的位置(从后往前找,假设为x),然后把新的key放在这个空的位置x上,并且让hash表中hash(key)处的节点的nk.next指向x。这个意思就是,假如有冲突的话,不用重新分配空间来存储冲突的key,而是利用hash表上未用过的空格来存储。但是这样会引入另外一个问题,本来key是不应该放在x的,假设有另外一个key2,hash(key2)算出来的位置也在x的话,那就表示本来x这个位置应该是给key2的,但是由于x被key占用了,导致key2没地方放了。这时候lua的处理方式是把key放到另外一个空格,然后让key2占回x。当hash表已经没有空格的时候,lua就会resize这个hash表。这样做的好处主要是不用动态申请内存空间,hash表初始化的时候有多少内存空间就用多少,不够就resize这个hash表。
    下面来看看table的具体实现,见Ltable.c:
    初始化table:
    Table *luaH_new (lua_State *L, int narray, int nhash) {
    Table *t = luaM_new(L, Table);
    luaC_link(L, obj2gco(t), LUA_TTABLE);
    t->metatable = NULL;
    t->flags = cast_byte(~0);
    /* temporary values (kept only if some malloc fails) */
    t->array = NULL;
    t->sizearray = 0;
    t->lsizenode = 0;
    t->node = cast(Node *, dummynode);
    setarrayvector(L, t, narray);
    setnodevector(L, t, nhash);
    return t;
    }
    一目了然,无需多说,array和hash部分默认都是0,然后用narray和nhash来初始化array和hash部分。array部分就初始化为narray长度的数组,hash部分就初始化为2^ceil(log2(nhash))长度的哈希表。table的哈希表的长度必须是2的幂,至于增长规则会在后面说到。
    table插入key:
    /*
    ** inserts a new key into a hash table; first, check whether key's main
    ** position is free. If not, check whether colliding node is in its main
    ** position or not: if it is not, move colliding node to an empty place and
    ** put new key in its main position; otherwise (colliding node is in its main
    ** position), new key goes to an empty position.
    */
    static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
    Node *mp = mainposition(t, key);
    if (!ttisnil(gval(mp)) || mp == dummynode) {
    Node *othern;
    Node *n = getfreepos(t);  /* get a free place */
    if (n == NULL) {  /* cannot find a free place? */
    rehash(L, t, key);  /* grow table */
    return luaH_set(L, t, key);  /* re-insert key into grown table */
    }
    lua_assert(n != dummynode);
    othern = mainposition(t, key2tval(mp));
    if (othern != mp) {  /* is colliding node out of its main position? */
    /* yes; move colliding node into free position */
    while (gnext(othern) != mp) othern = gnext(othern);  /* find previous */
    gnext(othern) = n;  /* redo the chain with `n' in place of `mp' */
    *n = *mp;  /* copy colliding node into free pos. (mp->next also goes) */
    gnext(mp) = NULL;  /* now `mp' is free */
    setnilvalue(gval(mp));
    }
    else {  /* colliding node is in its own main position */
    /* new node will go into free position */
    gnext(n) = gnext(mp);  /* chain new position */
    gnext(mp) = n;
    mp = n;
    }
    }
    gkey(mp)->value = key->value; gkey(mp)->tt = key->tt;
    luaC_barriert(L, t, key);
    lua_assert(ttisnil(gval(mp)));
    return gval(mp);
    }
    之前已经说过了,就是一个利用哈希表空洞来存储冲突节点的算法。其中有一些要注意的地方,首先是getfreepos这个函数:
    static Node *getfreepos (Table *t) {
    while (t->lastfree-- > t->node) {
    if (ttisnil(gkey(t->lastfree)))
    return t->lastfree;
    }
    return NULL;  /* could not find a free place */
    }
    这个函数会从后往前搜索hash表的空洞,找到的话就返回指向这个空洞的指针。lastfree在创建hash表的时候指向hash表最后一个元素。通过getfreepos可以知道hash表究竟还有没有空洞,如果没有的话,就要调用rehash来重新调整哈希表的大小了:
    static void rehash (lua_State *L, Table *t, const TValue *ek) {
    int nasize, na;
    int nums[MAXBITS+1];  /* nums[i] = number of keys between 2^(i-1) and 2^i */
    int i;
    int totaluse;
    for (i=0; i<=MAXBITS; i++) nums[i] = 0;  /* reset counts */
    nasize = numusearray(t, nums);  /* count keys in array part */
    totaluse = nasize;  /* all those keys are integer keys */
    totaluse += numusehash(t, nums, &nasize);  /* count keys in hash part */
    /* count extra key */
    nasize += countint(ek, nums);
    totaluse++;
    /* compute new size for array part */
    na = computesizes(nums, &nasize);
    /* resize the table to new computed sizes */
    resize(L, t, nasize, totaluse - na);
    }
    rehash中,nums是用来统计key的数量分布的,他的定义也说的很清楚
    int nums[MAXBITS+1];  /* nums[i] = number of keys between 2^(i-1) and 2^i */
    numusearray和numusehash的目的是统计数组和hash表部分key的使用情况,把它更新到nums里面去。然后根据nums计算要申请多大的数组部分以及多大的hash表,算法在computesizes处:
    static int computesizes (int nums[], int *narray) {
    int i;
    int twotoi;  /* 2^i */
    int a = 0;  /* number of elements smaller than 2^i */
    int na = 0;  /* number of elements to go to array part */
    int n = 0;  /* optimal size for array part */
    for (i = 0, twotoi = 1; twotoi/2 < *narray; i++, twotoi *= 2) {
    if (nums[i] > 0) {
    a += nums[i];
    if (a > twotoi/2) {  /* more than half elements present? */
    n = twotoi;  /* optimal size (till now) */
    na = a;  /* all elements smaller than n will go to array part */
    }
    }
    if (a == *narray) break;  /* all elements already counted */
    }
    *narray = n;
    lua_assert(*narray/2 <= na && na <= *narray);
    return na;
    }
    nums就是key的分布情况,narray是所有key里面,可以放在array里面的key的个数(array大小最大只能到2^26,并且如果key不是整数不能放在array里)。computesizes的算法,简单来说就是找出一个最小的i,使其满足小于2^i的key的个数大于2^(i-1),这个2^i就是数组部分的大小。如果找不到满足条件的i,数组部分长度为0。这样做的话,就保证了数组部分包含尽量多的元素,同时使用率在一半以上。
    computesizes算完之后就调用resize把hash表和array都重新分配一次,lua的resize和redis相比简单很多,一次过就把元素都重新插入到新的hash表和数组里面去了。注意:由于插入元素会导致rehash,rehash会重新调整元素是放在数组还是放在hash表,所以一个元素无论原来是在数组还是hash表,rehash后都不能确定它是在数组还是hash表,这就说明了为啥对一个当作数组使用的table的指定key赋值后,ipairs遍历这个table的结果通常不靠谱。
    table的查找:
    /*
    ** main search function
    */
    const TValue *luaH_get (Table *t, const TValue *key) {
    switch (ttype(key)) {
    case LUA_TNIL: return luaO_nilobject;
    case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key));
    case LUA_TNUMBER: {
    int k;
    lua_Number n = nvalue(key);
    lua_number2int(k, n);
    if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
    return luaH_getnum(t, k);  /* use specialized version */
    /* else go through */
    }
    default: {
    Node *n = mainposition(t, key);
    do {  /* check whether `key' is somewhere in the chain */
    if (luaO_rawequalObj(key2tval(n), key))
    return gval(n);  /* that's it */
    else n = gnext(n);
    } while (n);
    return luaO_nilobject;
    }
    }
    }
    如果key是string就调用luaH_getstr,如果是整数就调用luaH_getnum,默认的情况下找到key所在的节点,然后找到节点指向的链表中这个key的位置返回。luaH_getstr和luaH_getnum其实也是这个过程,只不过对string和number的哈希算法不同,number也有可能会放在数组部分而已。
    table的遍历:
    table的遍历分为ipairs和pairs,ipairs遍历数组部分,pairs遍历整个table。ipairs遍历顺序就是从0开始一次加1往后遍历table的数组部分。pairs的遍历实际上是调用luaH_next:
    int luaH_next (lua_State *L, Table *t, StkId key) {
    int i = findindex(L, t, key);  /* find original element */
    for (i++; i < t->sizearray; i++) {  /* try first array part */
    if (!ttisnil(&t->array[i])) {  /* a non-nil value? */
    setnvalue(key, cast_num(i+1));
    setobj2s(L, key+1, &t->array[i]);
    return 1;
    }
    }
    for (i -= t->sizearray; i < sizenode(t); i++) {  /* then hash part */
    if (!ttisnil(gval(gnode(t, i)))) {  /* a non-nil value? */
    setobj2s(L, key, key2tval(gnode(t, i)));
    setobj2s(L, key+1, gval(gnode(t, i)));
    return 1;
    }
    }
    return 0;  /* no more elements */
    }
    算法一目了然,先查数组部分,然后查哈希表,找到当前key的下一个key。所以遍历的结果是先遍历数组部分,然后遍历哈希表部分,哈希表部分实际上就是把node顺序遍历一次。
    分析到这里,已经基本知道原因了。number在lua里面是用double表示的,100001,100002,100003,100004,100005必定不能满足数组部分50%使用率的要求,所以都是放在哈希表里面。容纳这5个元素的哈希表的大小是8。lua的哈希number的哈希算法如下所示:
    /*
    ** hash for lua_Numbers
    */
    static Node *hashnum (const Table *t, lua_Number n) {
    unsigned int a[numints];
    int i;
    if (luai_numeq(n, 0))  /* avoid problems with -0 */
    return gnode(t, 0);
    memcpy(a, &n, sizeof(a));
    for (i = 1; i < numints; i++) a[0] += a[i];
    return hashmod(t, a[0]);
    }
    其中hashmod是计算a[0]%(哈希表大小-1)然后取t中这个位置的元素。所以100005之所以排在第一位是因为hashmod中计算出来100005的位置比其他元素的位置靠前,所以他放在靠前的位置,这样在pairs的时候,会首先遍历到100005。至此谜底完全解开。
    http://yulinlu.blog.163.com/blog/static/5881569820117351731462/

  • 相关阅读:
    hihoCoder #1062 : 最近公共祖先·一
    hihoCoder #1050 : 树中的最长路
    hihoCoder #1049 : 后序遍历
    108 Convert Sorted Array to Binary Search Tree 将有序数组转换为二叉搜索树
    107 Binary Tree Level Order Traversal II 二叉树的层次遍历 II
    106 Construct Binary Tree from Inorder and Postorder Traversal 从中序与后序遍历序列构造二叉树
    105 Construct Binary Tree from Preorder and Inorder Traversal 从前序与中序遍历序列构造二叉树
    104 Maximum Depth of Binary Tree 二叉树的最大深度
    102 Binary Tree Level Order Traversal 二叉树的层次遍历
    101 Symmetric Tree 判断一颗二叉树是否是镜像二叉树
  • 原文地址:https://www.cnblogs.com/byfei/p/6389908.html
Copyright © 2011-2022 走看看