UThash 的数据结构

来源:互联网 发布:砸金蛋抽奖软件 编辑:程序博客网 时间:2024/06/02 02:14

UThash 的数据结构

简介:

由于项目的需要,需要在一个嵌入式平台(用C语言)上用到hash map这个数据结构。于是搜索到开源的Uthash。Uthash 是一个C语言开发的hash map工具。其特点是用宏定义了所需要的对map的基本操作,如 插入、删除、查找和遍历。对应地,在uthash中采用 HASH_ADD、HASH-DELETE、HASH_FIND和HASH_ITER宏来操作,非常方便。 Uthash的高级功能也很好用,比如实现一个数据结构对应两个hash map等等,对于int型和string类型也有更简化的操作。对于Uthash的其他特性和使用方法在此不再加以赘述,请参考Uthash的指导文档,写得非常详细。下面仅仅介绍一下Uthash的数据结构。


关于uthash的数据结构,我起初是参考:

http://blog.csdn.net/devilcash/article/details/7230733

但是在仔细看了源代码之后发现这个博客的解释并不完全正确(可能是对应的版本不一样)。下面是我自己的解释(我采用的版本是1.9.8)。参考图见本文的最后部分。建议先看代码再看下图,更能加深理解。


数据结构:

//Uthash的三个数据结构://1. UT_hash_bucket作用提供根据hash进行索引。 typedef struct UT_hash_bucket {   struct UT_hash_handle *hh_head;   unsigned count;   unsigned expand_mult;} UT_hash_bucket;//2. UT_hash_table可以看做hash表的表头。typedef struct UT_hash_table {   UT_hash_bucket *buckets;   unsigned num_buckets, log2_num_buckets;   unsigned num_items;   struct UT_hash_handle *tail; /* tail hh in app order, for fast append    */   ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */    unsigned ideal_chain_maxlen;   unsigned nonideal_items;                unsigned ineff_expands, noexpand;   uint32_t signature; /* used only to find hash tables in external analysis */#ifdef HASH_BLOOM   uint32_t bloom_sig; /* used only to test bloom exists in external analysis */   uint8_t *bloom_bv;   char bloom_nbits;#endif} UT_hash_table;//3. UT_hash_handle,用户自定义数据必须包含的结构。typedef struct UT_hash_handle {   struct UT_hash_table *tbl;   void *prev;                       /* prev element in app order      */   void *next;                       /* next element in app order      */   struct UT_hash_handle *hh_prev;   /* previous hh in bucket order    */   struct UT_hash_handle *hh_next;   /* next hh in bucket order        */   void *key;                        /* ptr to enclosing struct's key  */   unsigned keylen;                  /* enclosing struct's key len     */   unsigned hashv;                   /* result of hash-fcn(key)        */} UT_hash_handle;


分析:


重要的是hash_handle的结构体,注意里面的四个指针: 
void *prev;                       /* prev element in app order      */   void *next;                       /* next element in app order      */   struct UT_hash_handle *hh_prev;   /* previous hh in bucket order    */   struct UT_hash_handle *hh_next;   /* next hh in bucket order        */

注意其注释。那么可以看到实际上在uthash中维护了两个链表,一个是app的链表,可以推测应该是按照插入的顺序连起来的。另外一个是和bucket相关的,同样可以推测该bucket应该就是UT_hash_bucket的bucket。


可以根据 HASH_ADD()操作来分析这个数据结构的用法。上面参考文章的图除了在上述四个指针处出错外,其他地方是正确的。

#define HASH_ADD(hh,head,fieldname,keylen_in,add) \HASH_ADD_KEYPTR(hh,head,&((add)->fieldname),keylen_in,add)

#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add) \do { \unsigned _ha_bkt; \(add)->hh.next = NULL; \(add)->hh.key = (char*)keyptr; \(add)->hh.keylen = (unsigned)keylen_in; \if (!(head)) { \head = (add); \(head)->hh.prev = NULL; \HASH_MAKE_TABLE(hh,head); \} else { \(head)->hh.tbl->tail->next = (add); \(add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail); \(head)->hh.tbl->tail = &((add)->hh); \} \(head)->hh.tbl->num_items++; \(add)->hh.tbl = (head)->hh.tbl; \HASH_FCN(keyptr,keylen_in, (head)->hh.tbl->num_buckets, \(add)->hh.hashv, _ha_bkt); \HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt],&(add)->hh); \HASH_BLOOM_ADD((head)->hh.tbl,(add)->hh.hashv); \HASH_EMIT_KEY(hh,head,keyptr,keylen_in); \HASH_FSCK(hh,head); \} while(0)

对于每个用户定义的hash_map来说,包含N个用户建立的表项。在第一次HASH_ADD的时候, 内部进行了初始化(内部调用了HASH_MAKE__TABLE()操作),通过调用uthash_malloc函数, 该操作分配了1个 UT_hash_table结构体的空间,然后分配了 32 个UT_hash_bucket的空间(起始默认是32个bucket,可以通过修改宏
HASH_INITIAL_NUM_BUCKETS
 来修改)。

代码是:

#define HASH_MAKE_TABLE(hh,head) \do { \(head)->hh.tbl = (UT_hash_table*)uthash_malloc( \    sizeof(UT_hash_table)); \if (!((head)->hh.tbl)) { uthash_fatal( "out of memory"); } \memset((head)->hh.tbl, 0, sizeof(UT_hash_table)); \(head)->hh.tbl->tail = &((head)->hh); \(head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS; \(head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2; \(head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head); \(head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc( \HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \if (! (head)->hh.tbl->buckets) { uthash_fatal( "out of memory"); } \memset((head)->hh.tbl->buckets, 0, \HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \HASH_BLOOM_MAKE((head)->hh.tbl); \(head)->hh.tbl->signature = HASH_SIGNATURE; \} while(0)

回到 HASH_ADD_KEYPTR中去,

分析前面部分,可以看出新建的项add 是通过 prev 和next连接进app链表关系的,也不难看出是插入到链表的后面的。对于每个新建项来说,其

(add)->hh.tbl = (head)->hh.tbl;
所以每个新建项的 tbl字段都指向 HASH_MAKE_TABLE中创建的UT_hash_table结构体。


注意UT_hash_table结构体有个字段是tail, 从HASH_ADD_KEYPTR中可以看出,这个tail总是指向app链表的尾部节点。

有tail的好处不言而喻就是为了往链表的尾部插入新节点更方便。


下面分析HASH_ADD_KEYPTR内部的HASH_ADD_TO_BKT():

/* add an item to a bucket */#define HASH_ADD_TO_BKT(head,addhh) \do { \head.count++; \(addhh)->hh_next = head.hh_head; \(addhh)->hh_prev = NULL; \if (head.hh_head) { (head).hh_head->hh_prev = (addhh); } \(head).hh_head=addhh; \if (head.count >= ((head.expand_mult+1) * HASH_BKT_CAPACITY_THRESH) \&& (addhh)->tbl->noexpand != 1) { \HASH_EXPAND_BUCKETS((addhh)->tbl); \} \} while(0)

后半部分是bucket扩展的部分,类似于很多容器的扩容操作,对此不做详细分析。仅分析前半部分。

(addhh)->hh_next = head.hh_head;
可以看出是插入到bucket链表的头部的,这与插入到app链表的尾部不同。然后修改头部的指针指向新的节点。


图示:

根据上面的理解不难画出下图。 为了方便看出 app链表和bucket链表,分成了两个部分。实际上是合在一块的。

这个图说明了3种结构体的关系,以及在app链表关系。新的节点插入在app链表的尾部。




这个图显示了bucket和它们的关系。假设左边的两个节点求得的hash值是一致的,那么会被分配到同样的bucket里面去。左上的节点先加入,左下的节点后加入。

可以看出节点是加入到bucket链表的头部的。


其他:

在根据key和key_len计算出hash值后,存在hashv处。通过

#define HASH_TO_BKT( hashv, num_bkts, bkt ) \do { \bkt = ((hashv) & ((num_bkts) - 1)); \} while(0)

来将其分配到bucket中去的。可以看到每个用户定义的项对应于1个bucket,含有相同hashv的项对应同一个bucket,一个bucket链表里面的各个节点其hashv有可能相同也有可能不同。


#define DECLTYPE_ASSIGN(dst,src) \do { \(dst) = DECLTYPE(dst)(src); \} while(0)
将src强制转化成dst的类型,再把值赋给dst;


/* calculate the element whose hash handle address is hhe */#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho)))
经常通过这个求出用户自定义结构体的首地址。因为要满足对用户自定义的类型的兼容性,所以采用了void * 类型。

UT_hash_handle结构体中的prev和next都是void * 类型的,也是这个原因。


根据上面的情况,不难猜出:

HASH_ITER  是通过遍历app链表实现的

HASH_FIND 是通过遍历 bucket链表实现的




链接:

uthash的User Guide:

http://troydhanson.github.io/uthash/userguide.html

uthash的下载:

https://github.com/troydhanson/uthash/blob/master/src/uthash.h