哈希表+双向链表的组合使用

在一个老牌通信公司工作几年,做的核心网项目开发,整天关注的是call flow,所参与的项目基本和MM和Call Control相关,

大多重点在项目开发逻辑,以及信令的解析和分析上。

公司内部代码有很多优秀的设计细节,和架构方式,只不过平时在此宏观的架构的基础上添加新代码,使用优秀简洁的接口,很少

仔细分析这些代码的优势所在。其中包括网络通信,进程间通信,以及多线程,共享内存,进程管理,状态机,以及定时器,

各种log机制。

最近有了一些闲暇时间,准备细细阅读基础代码,将一些优秀的设计理念和方法,加以整理总结。


首先看一下常用的hash表与双向链表结组合使用,这样的组合一般使用在网络通讯中,接收端收到大量的新数据消息,

将消息先存储到hash中,查询快捷,方便。


哈希表是由一个表头加上若干个节点组成,每个节点都是一个双向链表,数据对象实际是存储在哈希表节点的双向链表节点中。

由于每个节点都是一个双向链表,链表的每个节点中存储数据。

这样的好处是,如果随机产生的hash值如果相同,也可以使用,也不用重新产生命中的相同的hash值,可以直接存入对应的双向链表中。

这需要一个很好的散列化的hash函数,将数据对象均匀分布到节点的链表中。


哈希表的表头中定义了:

1. 哈希表的节点的最大个数变量

2. 产生哈希值的哈希函数指针变量的定义

3. 用于查找的对比函数指针变量的定义

4. 定义存储节点的数组

5. 存储数据对象的个数

双向链表定义:

1. 带有双向指针的表头

2. 带有双向指针的表尾

3. 带有双向链表中存储数据的个数


哈希表的表头定义如下:

/**
 * data structure of hash table
 */
typedef struct _HASHTABLE
{
        /// static configurable data of hash table
        int             numBuckets;     /// number of bucket in this hash table
        HASHFUNCPTR     hashFunc;       /// hash function from key to bucket
        COMPFUNCPTR     cmpFunc;        /// comparison function to the key of the object
        DLIST_P         bucketArray;    /// array of buckets in this hash table

        /// dynamic data of hash table
        int             numObjects;     /// number of objects in this hash table
} HASHTABLE_T, *HASHTABLE_P;


双向链表的定义如下:

/**
 *
 * The double linked list is used to manage the list of objects for specific purpose.
 * It allows the user to insert the object into or remove it from the list dynamically.
 *
 * To link them together, the object MUST contain GEN_NODE_T as the first element in its
 * structure, so that we can cast the pointer to the object to the GEN_NODE_P. 
 *
 * The structure of double linked list looks like the following.
 *
 *		+----------+         +----------+                         +----------+
 *     NULL <---+-- prev   |<--------+-- prev   |<--------       <--------+-- prev   |
 *		+----------+         +----------+           ...           +----------+
 *		|   next --+-------->|   next --+-------->       -------->|   next --+--->NULL
 *		+----------+         +----------+                         +----------+
 *		|   data   |         |   data   |                         |   data   |
 *		+----------+         +----------+                         +----------+
 *		     ^                                                         ^
 *        	     |                                                         |
 *   		     |                                                         |       
 *  		 head(list)                                                tail(list)    
 *
 */

/**
 * data structure of double linked list
 */
typedef struct _DLIST
{
	/// dynamic data of this list
	GEN_NODE_P	head;		/// pointer to the head node of this list
	GEN_NODE_P	tail;		/// pointer to the tail node of this list
	int		numObjects;	/// current number of nodes in this list
} DLIST_T, *DLIST_P;

/**
 * data structure of general purpose node used in double linked list
 *
 */
typedef struct _GEN_NODE {
        struct _GEN_NODE        *prev;          /// pointer to the previous node in the list
        struct _GEN_NODE        *next;          /// pointer to the next node in the list
} GEN_NODE_T, *GEN_NODE_P;



哈希表的创建:创建函数中有带入两个函数指针类型的函数,一个是hash函数,一个是对比函数,在头文件中事先定义好对应的函数指针。

哈希函数指针的定义

typedef unsigned int    (* HASHFUNCPTR)(const void *key);
对比函数指针的定义
typedef int     (* COMPFUNCPTR)(const void * obj, const void *key);

HASHTABLE_P HashTable_create(int numBuckets, HASHFUNCPTR hashFunc, COMPFUNCPTR cmpFunc)
{
        HASHTABLE_P ht = NULL;

        assert((hashFunc != NULL) && (cmpFunc != NULL) && (numBuckets > 0));

        ht = (HASHTABLE_P)calloc(1, sizeof(HASHTABLE_T) + sizeof(DLIST_T) * numBuckets);

        if (ht != NULL) {
                ht->numBuckets = numBuckets;
                ht->hashFunc = hashFunc;
                ht->cmpFunc = cmpFunc;
                ht->bucketArray = (DLIST_P)(ht + 1);
        }

        return ht;
}

这个函数是创建整个哈希表,为哈希表申请内存空间,包括哈希表的表头,还同时创建了numBuckets个双向链表。

在此用的申请空间的函数是calloc,在申请完空间后,会自动将空间清空。这是一个好的使用方法。

将哈希函数和对比函数的函数指针,赋入函数。

具体使用方法如下:

/* Max Number of Active CSFB CALL in Queue */
#define MAX_CSFB_CALL           4096

        g_pPgrHash  = HashTable_create(MAX_CSFB_CALL, s102PgrHash, s102PgrCmp);</span>
        if (g_pPgrHash  == NULL)
        {
                UX_ID_ELOG(ES1020623, "Unable to HashTable_create g_pPgrHash \n");
                UXTPCU(A21_KILL_SIG);
        }



销毁整个哈希表:销毁整个hash表,可以先调用清空此哈希表中所有数据的值,然后将申请的hash表的内存空间free掉。

void HashTable_destroy(HASHTABLE_P ht, FREEFUNCPTR freeFunc)
{
        assert(ht != NULL);

        // delete all the object from each bucket
        HashTable_delAllObjs(ht, freeFunc);

        // release the memory for hashtable
        free(ht);

        return;
}



哈希表插入数据:

可以根据查找函数HashTable_getBucket(),此函数的功能是根据key值,调用hash函数,产生hash值,返回对应数组中双向链表的地址。

然后调用双向链表的插入函数,将数据加入到双向链表的表尾处。

int HashTable_addObj(HASHTABLE_P ht, void *key, void *obj)
{
        DLIST_P bucket = NULL;

        assert((ht != NULL) && (key != NULL) && (obj != NULL));

        bucket = HashTable_getBucket(ht, key);
        if (bucket == NULL) return HASHTABLE_ERROR_HASHFAIL;

        if (DList_addObjTail(bucket, obj) != DLIST_NO_ERROR) {
                return HASHTABLE_ERROR_INSERTFAIL;
        }

        ht->numObjects++;
        return HASHTABLE_NO_ERROR;
}


双向链表添加数据的函数如下:

int DList_addObjTail(DLIST_P list, void *obj)
{
        assert((list != NULL) && (obj != NULL));

        GEN_NODE_P node = (GEN_NODE_P)obj;
        node->prev = list->tail;
        node->next = NULL;

        if (list->tail == NULL) {
                if ((list->head != NULL) || (list->numObjects != 0)) {
                        return DLIST_ERROR_INCONSISTENT;
                }

                list->head = node;
        } else {
                node->prev->next = node;
        }

        list->tail = node;
        list->numObjects++;

        return DLIST_NO_ERROR;
}

OBJ即为插入的数据对象,此数据对象有个要求,此数据结构的第一个变量必须是GEN_NODE_T      link;   

双向链表可以从head往里插入数据,也可从tail处插入数据。

此次是从tail处插入数据。

1. 首先将数据对象的首地址强制转换成节点类型,这就是为什么数据对象的结构体的第一个变量为什么必须是GEN_NODE_T 类型的缘故,将数据前后链接。

2. 从结尾处插入数据,将此节点的前驱指向双向链表的tail,将此节点的后继设为空。

3. 由于是双向链表,前一个数据,需要将后继改为新加入的节点,如果此链表为空,可直接将链表的头指向此数据对象节点。

4.从结尾出插入数据,新加入的数据即为tail,将链表的tail指向新加入的数据节点

5. 将链表中数据对象的个数加1


使用方法如下:

                       // add this csr into hash table with given key
                        retVal = HashTable_addObj(g_pCsrHash, (void *)mnidPtr, (void *)&g_s102csr[objId]);

                        if (retVal != HASHTABLE_NO_ERROR) {
                                UX_ID_ELOG( ES1020576, "Failed to call HashTable_addObj to add the csr index %d : retval: %d", objI\
d, retVal);
                                /// free the Correlation Id for other request message
                                Allocator_freeObject(g_pCsrIdleList, objId);
                                index = -1;
                        }


哈希表查找数据:

void *HashTable_findObj(HASHTABLE_P ht, void *key, DLIST_P *bucket)
{
        DLIST_P bkt = NULL;
        void    *obj = NULL;

        assert((ht != NULL) && (key != NULL));

        bkt = HashTable_getBucket(ht, key);
        if (bkt != NULL) {
                obj = DList_findObj(bkt, ht->cmpFunc, key);
        }

        if (bucket != NULL) *bucket = bkt;

        return obj;
}

根据查找函数HashTable_getBucket 获取key值应该放得节点位置

根据key值,调用hash函数,产生hash值,返回对应数组中双向链表的地址。

根据对比key,在此双向链表中查询此对象key值,如果找到,返回对应节点的地址。

根据节点地址,可获取此节点中对象的数据。

从双向链表中查找数据的函数如下:

void* DList_findObj(DLIST_P list, COMPFUNCPTR cmpFunc, void *key)
{
        assert((list != NULL) && (cmpFunc != NULL) && (key != NULL));

        GEN_NODE_P      node = list->head;
        int             count = 0;

        while ((node != NULL) && (count < list->numObjects)) {
                if ((* cmpFunc)((void *)node, key) == 0) return (void *)node;
                count++;
                node = node->next;
        }

        return NULL;
}
通过对比函数将key值和链表中的数据key值进行对比,返回节点地址。


哈希表删除一条数据:

void *HashTable_removeObj(HASHTABLE_P ht, void *key)
{
        DLIST_P bucket = NULL;
        void    *obj = NULL;

        assert((ht != NULL) && (key != NULL));

        obj = HashTable_findObj(ht, key, &bucket);

        if (obj != NULL) {
                if (bucket != NULL) {
                        DList_removeObj(bucket, obj);
                        ht->numObjects--;
                } else {
                        return NULL;
                }
        }

        return obj;
}

在此哈希表中删除一条数据包含了哈希表查找数据,然后再双向链表中将此数据删除,hash表中数据总个数减1。


双向链表删除一个对象的函数如下:

void DList_removeObj(DLIST_P list, void *obj)
{
        assert(list != NULL);

        GEN_NODE_P node = (GEN_NODE_P)obj;

        if (node == NULL) return;

        if (node->prev == NULL) {
                // if the object is the head of list, set the head to the next object
                list->head = node->next;
        } else {
                // otherwise, let the previous one links to the next object
                node->prev->next = node->next;
        }

        if (node->next == NULL) {
                // if the object is the tail of list, set the tail to the previous object
                list->tail = node->prev;
        } else {
                // otherwise, let the next one links to the previous object
                node->next->prev = node->prev;
        }

        // clear the linkage of this object
        node->prev = NULL;
        node->next = NULL;

        list->numObjects--;
        return;
}

删除一个对象的流程如下:

1. 首先将数据对象的首地址强制转换成节点类型,这就是为什么数据对象的结构体的第一个变量为什么必须是GEN_NODE_T 类型的缘故,将数据前后链接,便于操作。

2. 如果要删除的节点的前驱是空,表明此节点为head,将此节点的后继设为head即可

3. 如果要删除的节点的前驱不是空,将此节点前驱的后继指向此节点的后继。

4. 如果要删除的节点的后继是空,表明此节点为tail,将此节点的前驱设为tail即可

5.  如果要删除的节点的后继不是空,将此节点后继的前驱设为此节点的前驱

6. 将此节点前驱后继,设为null

7.链表中的数据总个数减1


清空整个哈希表:

void HashTable_delAllObjs(HASHTABLE_P ht, FREEFUNCPTR freeFunc)
{
        int i;

        assert(ht != NULL);

        for (i=0; i<ht->numBuckets; i++) {
                DList_delAllObjs(&ht->bucketArray[i], freeFunc);
        }

        ht->numObjects = 0;

        return;
}

获取哈希表中存储数据对象的个数:

int HashTable_getSize(HASHTABLE_P ht)
{
        assert(ht);

        if (ht->numObjects < 0) return HASHTABLE_ERROR_INCONSISTENT;
        return ht->numObjects;
}

根据key值获取哈希表数据节点的位置:

DLIST_P HashTable_getBucket(HASHTABLE_P ht, void *key)
{
        int slot = 0;

        assert((ht != NULL) && (key != NULL));

        slot = (* ht->hashFunc)(key);
        if ((slot < 0) || (slot >= ht->numBuckets)) return NULL;

        return &ht->bucketArray[slot];
}

根据key值,调用hash函数,产生hash值,返回对应数组中双向链表的地址。


以上为hash表的操作中套用双向链表的操作方法。

你可能感兴趣的:(哈希表+双向链表的组合使用)