Postfix下hash表的实现 (-)(原创)
前一段时间分析邮件系统Postfix的源代码,发现它的Hash表作得很有意思,今天得空,把我以前的笔记摘录和Postfix中Hash操作的源代码摘录出来,与大家分享。孟子曰:独乐乐,不如与众乐乐。希望我的一些心得对大家有帮助。
Hash表的功能和用法
Postfix提供的Util工具中提供了Hash表的管理功能(文件binhash.c和文件htable.c)。binhash.c实现的Hash表以二进制数据为主键,htable.c实现的Hash表以‘、0’结束的字符串为主键,二者就是这个差别,其它的算法、数据结构完全相同,调用方式类似。之所以需要有这两个不同的实现,是因为绝大多数的Hash表应用都以字符串为主键,但作为一种扩展功能,我们可以用其它C语言内建类型或者自定义数据类型为主键,那就要用到以二进制数据为主键的Hash表了。
因为这两种Hash表实现方式极其类似,所以我仅以二进制数据为主键的Hash表为例,描述Hash表的实现。二者调用方法的略微不同,将在下文中说明。
一.Hash表简介
Hash表存储类似{主键,键值}的数据,提供根据主键搜索键值的操作。和链表的依次查找方法不同,Hash表根据主键直接计算出数据的存储位置,因此,Hash表的搜索速度是相当快的,问题是,不同的主键,对应的存储位置可能相同,这就出现了地址冲突。解决方法是,将具有不同主键、相同存储位置的数据组织成一个链表,表头地址就是Hash表计算出来的存储位置。Hash表进行查找时,首先根据主键计算出存储位置,如果该地址的第一个节点不和我们要查找的数据匹配,那么继续搜索该节点的后继节点,找到与主键匹配的节点为止。因此,如果有地址冲突,Hash表就必须先定位,再搜索,这样的效率虽然比一次定位低很多,但和将所有数据放在一个链表中,从头找到尾相比,速度快多了。
但是Util的Hash表在解决地址冲突方面却使用了非常巧妙的方法。首先,用户建立指定长度的Hash表,每次输入数据时,Hash表都会检测是否有地址冲突。如果发现有地址冲突,Hash表自动将长度扩大到原来的2倍,重新安排数据的存储位置。按照这样的处理方式,Hash表基本上不会有冲突的地址,查找效率大大增强。但是如果使用这样的方法,就要求Hash表的定位算法,也就是根据主键计算存储位置的算法能够根据表的长度作自动调整。比如,Hash表的长度为n(n>0),那么定位算法根据任意主键计算出的值应该在[0,n-1]之间,而且数据应该是均匀分布的,如果Hash表的长度增加为2n,那么定位算法计算出的值应该在[0,2n-1]之间,同样是均匀分布的。Util的Hash表定位算法完全满足以上要求。
Util的Hash表还有一个非常有意思的功能,就是提供为用户自定义释放数据内存提供了接口。Hash表的数据也就是{主键,键值}对,主键无非就是二进制数据或者字符串,它的释放可以使用标准的free()函数或者free()函数的包装方式(比如Util中的myfree()函数)。但是对键值的释放就不是那么简单了,键值可以是用户定义的复杂的数据结构,它的释放也许不是一个简单的free()就能了结,因此,需要用户自己定义释放键值的方式。Util的Hash表提供了用户自定义释放数据的接口,大大增强了Hash表使用的灵活性。
二.Util中的Hash表的使用
Hash表的操作无非就是创建Hash表、增加数据、删除数据、查找数据、定位数据、释放Hash表、遍历Hash表等。在介绍这些功能之前,首先描述Hash表用到的数据结构。Hash表的结构定义在<binhash.h>和<htable.h>中。
typedef struct BINHASH_INFO {
char *key; /* lookup key */
int key_len; /* key length */ /*实用于二进制主键,字符串主键不需要*/
char *value; /* associated value */
struct BINHASH_INFO *next; /* colliding entry */
struct BINHASH_INFO *prev; /* colliding entry */
} BINHASH_INFO;
typedef struct BINHASH {
int size; /* length of entries array */
int used; /* number of entries in table */
/*如果used>size,那就表明有地址冲突,Hash表将自动增加表的长度,重新安排数据的存储位置*/
BINHASH_INFO **data; /* entries array, auto-resized */
/*data的安排很巧妙,比如,Hash表根据主键计算出的值是n,那么数据将存储在data+n上,如果又有一个主键计算出的值也是n,数据将储存在*(data+n)->next位置上,以后的数据依次类推,在(data+n)为首址位置上形成一个链表,存放所有主键计算值为n的的数据。*/
} BINHASH;
1.Hash表的创建 binhash_create()
BINHASH *binhash_create(int size)
参数:
size: Hash表的长度。如果size<13,Hash表会自动将长度增加为13。即Hash表的最小长度为13。
返回值:
新建立的Hash表。
2.输入数据 binhash_enter()
BINHASH_INFO *binhash_enter(BINHASH *table, const char *key, int key_len, char *value)
参数:
table: Hash表指针
key: 主键
key_len: 主键长度。如果是字符串主键,不需要这个参数。见<htable.h>
value: 键值
返回值:
输入数据后,返回该数据的存储点(BINHASH_INFO *)。
需要特别指出的是,键值value不仅仅是字符串,还可以是用户定义的复杂的数据结构,在使用时,进行强制的数据转型。更重要的是,value应该是已经分配内存的数据,别指望binhash_enter会为你分配内存。为什么要这样做,其实是很有道理的。只有用户自己才知道该怎样为自己的数据结构分配内存;同样的,也只有用户自己才知道该怎样为自己的数据结构释放内存。因此,在删除数据和释放Hash表时,必须用用户自定义的内存释放函数来处理数据内存的回收(见binhash_delete()和binhash_free())。
作者Wietse Venama无疑是位编程高手,结构和思维的严谨可见一瘢。象我这样的程序庸手也可以写出Hash表的实现,但就是写不出这样的水平。
3.查找数据 binhash_find()
char *binhash_find(BINHASH *table, const char *key, int key_len)
参数:
table: Hash表指针
key: 主键
key_len:主键长度
返回值:
根据主键,得到键值。如果没找到,返回NULL。
如果键值是用户自定义结构,使用时需要对返回值进行强制转型。
4.数据定位 binhash_locate()
BINHASH_INFO *binhash_locate(BINHASH *table, const char *key, int key_len)
功能和binhahs_find一样,只是返回的不是键值,而是包含需要查找数据的BINHASH_INFO结构指针。可以从BINHASH_INFO*得到所有数据信息。而且通过BINHASH_INFO->next,BINHASH_INFO->prev得到和它具有相同主键计算值的数据。
5.删除数据 binhash_delete()
void binhash_delete(BINHASH *table, const char *key, int key_len, void (*free_fn) (char *))
参数:
table: Hash表指针
key: 主键
key_len: 主键长度
free_fn: 释放数据(键值)内存的用户自定义释放函数
从Hash表中删除主键所指的数据,并且调用用户自定义释放函数,释放数据(键值)内存。还是那句话,用户自己定义的数据结构,由用户自己释放。而主键的释放,却可以简单地调用标准函数free()或free()的封装函数(如myfree())由Hash表释放。
6.释放Hash表 binhash_free()
void binhash_free(BINHASH *table, void (*free_fn) (char *))
参数:
table: Hash表指针
free_fn: 释放数据(键值)内存的用户自定义释放函数
删除Hash表中的所有数据,调用用户自定义释放函数,释放所有数据(键值)内存。同时,删除Hash表自身。
7.运行Hash表 binhash_walk()
void binhash_walk(BINHASH *table, void (*action) (BINHASH_INFO *, char *), char *ptr)
参数:
table: Hash表指针
action: 运行Hash表数据的函数指针
ptr: 提供给函数action的附加数据
调用用户自定义运行函数action,依次运行Hash表中所有的数据。函数action有一个附加数据结构ptr,可用可不用,如果要用的话,也不必把ptr仅仅看成是字符串,可以是更复杂的数据结构,在使用时进行数据转型。
Binhash_walk最适合于调试,看看Hash表里到底有些什么内容。
8.遍历Hash表 binhash_list()
BINHASH_INFO **binhash_list(BINHASH *table)
参数:
table: Hash表指针
返回值:
BINHASH_INFO**,包含Hash表中所有数据。BINHASH_INFO**的内存由Hash表分配,使用后别忘了该由用户释放。binhash_list的最典型应用方式应该是这样的:
{
BINHASH_INFO **hashlist,**ppbin;
BINHASH_INFO *pbin;
hashlist=binhash_list(table);
for (ppbin=hashlist;pbin=*ppbin; ppbin++)
{
while (*pbin)
{
ACTION(pbin); /*你愿意怎么操作BINHASH_INFO*?*/
pbin=pbin->next
}
}
free(hashlist); /*别忘释放hashlist,这是我们的份内事*/
}