Skip to the content.

环境 PHP5.6

PHP的数组使用哈希表(HashTable)来存储关联数据,PHP内核中的哈希表是十分重要的数据结构,PHP的大部分的语言特性都是基于哈希表实现的。

哈希表基本原理

哈希函数


伪代码

HASH_INDEX(ht, int key) {

    return key % ht->size;

}

操作接口

初始化


伪代码

int hash_init(HashTable *ht)

{

    ht->size        = HASH_TABLE_INIT_SIZE;

    ht->elem_num    = 0;

    ht->buckets     = calloc(ht->size, sizeof(Bucket *));

    return SUCCESS;

}

内容插入到哈希表中


伪代码(细节待完善)

int hash_insert(HashTable *ht, char *key, void *value)

{

    // 判断是否需要重哈希

    resize_hash_table_if_needed(ht);

    // 通过哈希函数获取bucket

    int index = HASH_INDEX(ht, key);

    Bucket *tmp_bucket = ht->buckets[index];

    // key已经存在,循环冲突的链表,如果找到key则更新其value

    while(tmp_bucket) {

        if(strcmp(key, tmp_bucket->key) == 0) {

            tmp_bucket->value = value;

            return SUCCESS;

        }

        tmp_bucket = tmp_bucket->next;

    }

    // 生成新的元素

    Bucket *bucket = (Bucket *)malloc(sizeof(Bucket));

    bucket->key   = key;

    bucket->value = value;

    bucket->next  = NULL;

    ht->buckets[index]= bucket;

    // 总的元素+1

    ht->elem_num += 1;

    return SUCCESS;

}

重哈希


伪代码

resize_hash_table_if_needed(HashTable *ht)

{

    // buckets的长度小于等于总的元素

    if(ht->size - ht->elem_num < 1) {

        hash_resize(ht);

    }

}

static int hash_resize(HashTable *ht)

{

    // 原来长度的两倍

    int org_size = ht->size;

    ht->size = ht->size * 2;

    ht->elem_num = 0;

    // 新的buckets

    Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket *));

    Bucket **org_buckets = ht->buckets;

    ht->buckets = buckets;

    int i = 0;

    for(i=0; i < org_size; ++i) {

        Bucket *cur = org_buckets[i];

        while(cur) {

            // 重哈希,重新插入

            hash_insert(ht, cur->key, cur->value);

            // 链表循环(冲突的hash值)

            cur = cur->next;

        }

    }

    free(org_buckets);

    return SUCCESS;

}

PHP中哈希表的实现

存储结构


typedef struct bucket {

    ulong h;            // 如果键是数字,那么h指的就是数字;如果键是字符串,那么h指的是字符串经过hash后获得int型值

    uint nKeyLength;    // hash关键字的长度,如果数组索引为数字,此值为0

    void *pData;        // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr

    void *pDataPtr;     // 如果是指针数据,此值会指向真正的vaule,同时上面pData会指向此值

    struct bucket *pListNext; // 整个hash表的下一个元素

    struct bucket *pListLast; // 整个hash表的上一个元素

    struct bucket *pNext;     // 同一个 Bucket内的下一个元素

    struct bucket *pLast;     // 同一个 Bucket内的上一个元素

    const char *arKey;        // 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长的结构体

} Bucket;

typedef struct _hashtable {

    uint nTableSize;            // hash Bucket的大小,最小为8, 以2n增长 nTableSize字段用于标示哈希表的容量

    uint nTableMask;            // nTableMask-1   索引取值的优化

    uint nNumOfElements;        // 整个hash table 中当前存在的元素个数,count() 函数会直接返回比值

    ulong nNextFreeElement;     // 下一个数字索引的位置

    Bucket *pInternalPointer;   /* Used for element traversal 当前遍历的指针 (foreach比for快的原因之一 )*/

    Bucket *pListHead;          // 存储数组头元素指针

    Bucket *pListTail;          // 存储数组尾元素指针

    Bucket **arBuckets;         // 存储hash数组

    dtor_func_t pDestructor;    // 在删除元素时执行的回调函数,用于资源的释放

    zend_bool persistent;       // 指出了Bucket内存的分配方式,如果persistent为true,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数

    unsigned char nApplyCount;  // 标记当前 hash Bucket被递归访问的次数(防止多次递归)

    zend_bool bApplyProtection; // 标记当前hash桶运行不允许多次访问,不允许访问时,最多只能递归3次

#if ZEND_DEBUG

    int inconsistent;

#endif

} HashTable;

哈希表的初始化 hash_init


Zend/zend_hash.c

ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)

{

    uint i = 3;

    SET_INCONSISTENT(HT_OK);

    if (nSize >= 0x80000000) {

        /* prevent overflow  防止溢出 */

        ht->nTableSize = 0x80000000;

    } else {

        while ((1U << i) < nSize) {

            i++;

        }

        ht->nTableSize = 1 << i;

    }

    ht->nTableMask = 0; /* 0意味着ht->arBuckets未初始化 */

    ht->pDestructor = pDestructor;

    ht->arBuckets = (Bucket**)&uninitialized_bucket;

    ht->pListHead = NULL;

    ht->pListTail = NULL;

    ht->nNumOfElements = 0;

    ht->nNextFreeElement = 0;

    ht->pInternalPointer = NULL;

    ht->persistent = persistent;

    ht->nApplyCount = 0;

    ht->bApplyProtection = 1;

    return SUCCESS;

}

计算哈希值


h = zend_inline_hash_func(arKey, nKeyLength);

nIndex = h & ht->nTableMask;

ht->nTableMask的大小为ht->nTableSize -1。 这里使用&操作

mask的作用就是将哈希值映射到槽位所能存储的索引范围内。

例如:某个key的索引值是21, 哈希表的大小为8,则mask为7,

    则求与时的二进制表示为: 10101 & 111 = 101 也就是十进制的5。

插入内容到哈希表

建议看源码 Zend/zend_hash.c 文件中 _zend_hash_add_or_update函数

总结

参考文献

哈希冲突: https://blog.csdn.net/qq_27093465/article/details/52269862

PHP中的hash算法: http://www.laruence.com/2009/07/23/994.html

PHP数组的Hash冲突实例:http://www.laruence.com/2011/12/30/2435.html

哈希表(HashTable):http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable

PHP的哈希表实现: http://www.php-internals.com/book/?p=chapt03/03-01-02-hashtable-in-php