Apache中的哈希表剖析(1)
Apache中的哈希表剖析(1) 3.4 哈希表3.4.1哈希表概述 作为线性数据结构,与前面所说的表格和队列等相比,哈希表无疑是查找速度比较快的一种。APR中较好的支持哈希表。APR中哈希表在文件apr_hash.h和apr_hash.c中实现。其数据结构定义如下: struct apr_hash_t {? apr_pool_t *pool; apr_hash_entry_t **array; apr_hash_index_t iterator; /* For apr_hash_first(NULL, ...) */ unsigned int count, max; apr_hashfunc_t hash_func; apr_hash_entry_t *free; /* List of recycled entries */ }; 与其余的数据结构类似,第一个成员通常是分配该结构的内存池。哈希表中的每一个元素都用apr_hash_entry_t结构描述,array指向一个数组,该数组中每一个元素都是一个指向apr_hash_entry_t类型的链表。为了方便对哈希表的迭代循环遍历,每个结构中都维持一个apr_hash_index_t用以辅助迭代,该成员的详细细节我们在后面的部分会深入讨论。 count用以记录当前整个哈希表中结点的总数目。max则是当前哈希表中允许的哈希值的最大值,反映到结构中就是array数组中的元素的个数。 hash_func则是哈希函数,通过该函数可以确定给定元素在哈希表中的索引,可以使用默认的哈希函数,也可以使用自定义的哈希函数。 现在我们来看一下哈希表元素数据结构apr_hash_entry_t,该结构定义如下: struct apr_hash_entry_t { apr_hash_entry_t *next; unsigned int hash; const void *key; apr_ssize_t klen; const void *val; }; hash是当前元素在哈希表中所对应的哈希索引值,key则是哈希项的键,value则是哈希项的值。哈希表中以键值key作为唯一的标识索引。如果键值存在冲突,那么这些冲突键将用链表关联起来。整个哈希表的结构可以用下面的图描述:
3.4.2哈希表创建3.4.2.1 零创建 APR中创建一个哈希表可以通过两种途径:apr_hash_make和apr_hash_make_custom实现: APR_DECLARE(apr_hash_t *) apr_hash_make(apr_pool_t *pool); APR_DECLARE(apr_hash_t *) apr_hash_make_custom(apr_pool_t *pool, apr_hashfunc_t hash_func); 两者的区别就是哈希算法的不同。对于apr_hash_make而言,它的主要的工作就是创建apr_hash_t结构,并对其中的成员进行初始化,其中哈希元素的个数被初始化为16个,同时使用默认的哈希算法apr_hashfunc_default,而apr_hash_make_custom则使用自定义的哈希函数hash_func。 unsigned int apr_hashfunc_default(const char *char_key, apr_ssize_t *klen) { unsigned int hash = 0; const unsigned char *key = (const unsigned char *)char_key; const unsigned char *p; apr_ssize_t i; if (*klen == APR_HASH_KEY_STRING) { for (p = key; *p; p++) { hash = hash * 33 + *p; } *klen = p - key; } else { for (p = key, i = *klen; i; i--, p++) { hash = hash * 33 + *p; } } return hash; } 对于给定的键值key,apr_hashfunc_default返回它在哈希表中的索引。默认哈希算法采用了目前最为流行的times 33哈希算法,目前该算法被广泛使用在多个软件项目包括perl和巴克利(Berkeley DB)数据库中。对于字符串而言这是目前所知道的最好的哈希算法,原因在于该算法的速度非常快,而且分类非常好。 不过你不愿意使用该索引算法而希望使用自己的,那么你可以使用apr_hash_make_custom函数,它接受一个自定义的哈希算法函数,并将其赋值给apr_hash_t结构内的func成员,从而取代默认算法函数。 apr_hashfunc_t函数指针定义如下: typedef unsigned int (*apr_hashfunc_t)(const char *key, apr_ssize_t *klen); 它需要两个参数,一个是需要进行哈希计算的键,另一个则是该键的长度。函数返回计算后的索引。 3.4.2.2 拷贝创建 与大部分数据结构一样,对于哈希表,APR也提供了拷贝创建方法,允许从一个已有的哈希表创建一个新的哈希表,拷贝函数声明如下: APR_DECLARE(apr_hash_t *) apr_hash_copy(apr_pool_t *pool,const apr_hash_t *orig) orig是源哈希表,在拷贝中所用所有的内存都来自内存池pool,拷贝后的哈希表由函数返回。 APR_DECLARE(apr_hash_t *) apr_hash_copy(apr_pool_t *pool,const apr_hash_t *orig) { apr_hash_t *ht; apr_hash_entry_t *new_vals; unsigned int i, j; ht = apr_palloc(pool, sizeof(apr_hash_t) + sizeof(*ht->array) * (orig->max + 1) + sizeof(apr_hash_entry_t) * orig->count); ht->pool = pool; ht->free = NULL; ht->count = orig->count; ht->max = orig->max; ht->hash_func = orig->hash_func; ht->array = (apr_hash_entry_t **)((char *)ht + sizeof(apr_hash_t)); new_vals = (apr_hash_entry_t *)((char *)(ht) + sizeof(apr_hash_t) + sizeof(*ht->array) * (orig->max + 1)); 尽管称之为哈希表拷贝,但是apr_hash_copy实现的仅仅是一种影像拷贝。之所以称之为影像拷贝,是因为尽管拷贝后的哈希表能够实现与源哈希表相同的功能,但是内部数据组织已经发生了变化,最大的变化就是从源哈希表的不连续的链表结构转换为连续的块状结构。 既然是块状数据结构,我们首先就必须考虑块状结构的大小,然后才能分配。新的块状结构的大小应该与原有的链表结构大小相等。总的大小包括三方面: 1)、apr_hash_t结构的大小sizeof(apr_hash_t) 2)、apr_hash_t结构内array数组的大小,数组的总元素个数为max+1,每一个元素都是一个 apr_hash_entry_t类型的指针,因此整个数组的大小为(orig->max+1)*sizeof(*ht->array),或者也可以写成(orig->max+1)*sizeof(apr_hash_entry_t*)。 3)、整个哈希表中apr_hash_entry_t类型结点的总数,其值由orig->count决定,每个结点的大小为sizeof(apr_hash_entry_t),故总大小为sizeof(apr_hash_entry_t) * orig->count。 一旦确定了总的需要分配的内存大小,APR将从内存池中一次性分配足够的连续内存。这些内存将被分为三部分:apr_hash_t部分、max+1个apr_hash_entry_t指针部分以及count个哈希元素的大小。因此内存一旦分配完毕,除了使用原有哈希表结构成员初始化新的哈希表成员包括pool、count、max以及hash_func等之外,最重要的就是设置指针指向后面的两个内存部分,初始化后的布局如下所示: 1. j =0; 2. for (i = 0; i <= ht->max; i++) { 3. apr_hash_entry_t **new_entry = &(ht->array[i]); u 4. apr_hash_entry_t *orig_entry = orig->array[i]; 5. while (orig_entry) { 6. *new_entry = &new_vals[j++]; 7. (*new_entry)->hash = orig_entry->hash; 8. (*new_entry)->key = orig_entry->key; 9. (*new_entry)->klen = orig_entry->klen; 10. (*new_entry)->val = orig_entry->val; 11. new_entry = &((*new_entry)->next); v 12. orig_entry = orig_entry->next;w 13. } 14. *new_entry = NULL; 15. } 16. return ht; 在源哈希表中,ht->array数组中的每一个元素都是一个apr_hash_entry_t类型的指针,指向的是一个链表,而到了目标哈希表中,该指针指向的则是一个数组。因此,拷贝的一个重要任务就是调整array数组中的各个指针将其指向new_vals内存的适当的部分。调整后的布局如下图所示。 调整过程包括三个大步骤,图示中用jkl进行标识: j array数组中的每一个元素都是一个指针,必须调整数组中的每一个指针指向new_vals部分的合适的位置。原则是:如果源哈希表中对应元素为NULL,则新指针也为NULL;如果对应的结点链表结点数目为n,则下一个索引指针与前一个偏移n*sizeof(apr_hash_entry_t)个。具体如灰色代码部分所示。 k 对每一个结点进行内容拷贝,如7.8.9.10行。 l 调整各个结点内部的next指针,指向它的直接后继结点,或者是紧靠它的下一个apr_hash_entry_t结点,或者是NULL。 new_entry = &((*new_entry)->next); 在上图中我们用虚线将链表结构和块状结构中对应得结点连接起来,以方便对比。