0

0

深入理解PHP内核(六)哈希表以及PHP的哈希表实现 - orlion

php中文网

php中文网

发布时间:2016-05-20 11:54:32

|

1046人浏览过

|

来源于php中文网

原创

原文链接:http://www.orlion.ga/241/

一、哈希表(HashTable)

    大部分动态语言的实现中都使用了哈希表,哈希表是一种通过哈希函数,将特定的键映射到特定值得一种数据

 

结构,它维护键和值之间一一对应关系。

立即学习PHP免费学习笔记(深入)”;

键(key):用于操作数据的标示,例如PHP数组中的索引或者字符串键等等。

槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数组真正存放的容器。

哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。

哈希冲突(hash collision):哈希函数将两个不同的key映射到同一个索引的情况。

 

    目前解决hash冲突的方法有两种:链接法和开放寻址法。

 

1、冲突解决

    (1)链接法

    链接法通过使用一个链表来保存slot值的方式来解决冲突,也就是当不同的key映射到一个槽中的时候使用链表

 

来保存这些值。(PHP中正是使用了这种方式);

    (2)开放寻址法

    使用开放寻址法是槽本身直接存放数据,在插入数据时如果key所映射到的索引已经有数据了,这说明有冲突,

 

这时会寻找下一个槽,如果该槽也被占用了则继续寻找下一个槽,直到找到没有被占用的槽,在查找时也是这样

 

2、哈希表的实现

    哈希表的实现主要完成的工作只有三点:

    * 实现哈希函数

    * 冲突的解决

    * 操作接口的实现

(1)数据结构

    首先需要一个容器来曹村我们的哈希表,哈希表需要保存的内容主要是保存进来的数据,同时为了方便的得知哈希表中存储的元素个数,需要保存一个大小字段,第二个需要的就是保存数据的容器。下面将实现一个简易的哈希表,基本的数据结构主要有两个,一个用于保存哈希表本身,另外一个就是用于实际保存数据的单链表了,定义如下:

typedef struct _Bucket
{
    char *key;
    void *value;
    struct _Bucket *next;
 
} Bucket;
 
typedef struct _HashTable
{
    int size;
    Bucket* buckets;
} HashTable;

上边的定义与PHP中的实现相似,为了简化key的数据类型为字符串,而存储的结构可以为任意类型。

Bucket结构体是一个单链表,这是为了解决哈希冲突。当多个key映射到同一个index的时候将冲突的元素链接起来

 

(2)哈希函数实现

我们采用一种最简单的哈希算法实现:将key字符串的所有字符加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了。

Cutout.Pro
Cutout.Pro

AI驱动的视觉设计平台

下载
static int hash_str(char *key)
{
    int hash = 0;
 
    char *cur = key;
 
    while(*(cur++) != '\0') {
        hash += *cur;
    }
 
    return hash;
}
 
// —使用这个宏来求得key在哈希表中的索引
#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)

PHP使用的哈希算法称为DJBX33A。为了操作哈希表定义了如下几个操作函数:

int hash_init(HashTable *ht);                               // 初始化哈希表
int hash_lookup(HashTable *ht, char *key, void **result);   // 根据key查找内容
int hash_insert(HashTable *ht, char *key, void *value);     // 将内容插哈希表中
int hash_remove(HashTable *ht, char *key);                  // 删除key所指向的内容
int hash_destroy(HashTable *ht);

下面以插入和获取操作函数为例:

int hash_insert(HashTable *ht, char *key, void *value)
{
    // check if we need to resize the hashtable
    resize_hash_table_if_needed(ht);    // 哈希表不固定大小,当插入的内容快占满哈希表的存储空间
                                        // 将对哈希表进行扩容,以便容纳所有的元素
    int index = HASH_INDEX(ht, key);    // 找到key所映射到的索引
 
    Bucket *org_bucket = ht->buckets[index];
    Bucket *bucket = (Bucket *)malloc(sizeof(Bucket)); // 为新元素申请空间
 
    bucket->key   = strdup(key);
    // 将值内容保存起来,这里只是简单的将指针指向要存储的内容,而没有将内容复制
    bucket->value = value;  
 
    LOG_MSG("Insert data p: %p\n", value);
 
    ht->elem_num += 1; // 记录一下现在哈希表中的元素个数
 
    if(org_bucket != NULL) { // 发生了碰撞,将新元素放置在链表的头部
        LOG_MSG("Index collision found with org hashtable: %p\n", org_bucket);
        bucket->next = org_bucket;
    }
 
    ht->buckets[index]= bucket;
 
    LOG_MSG("Element inserted at index %i, now we have: %i elements\n",
        index, ht->elem_num);
 
    return SUCCESS;
}

在查找时首先找到元素所在的位置,如果存在元素,则将链表中的所有元素的key和要查找的key依次对比,直到找到一致的元素,否则说明该值没有匹配的内容。

int hash_lookup(HashTable *ht, char *key, void **result)
{
    int index = HASH_INDEX(ht, key);
    Bucket *bucket = ht->buckets[index];
     if(bucket == NULL) return FAILED;
 
    // 查找这个链表以便找到正确的元素,通常这个链表应该是只有一个元素的,也就不同多次循环
    // 要保证这一点需要有一个合适的哈希算法。
    while(bucket)
    {
        if(strcmp(bucket->key, key) == 0)
        {
            LOG_MSG("HashTable found key in index: %i with  key: %s value: 
%p\n",
                index, key, bucket->value);
            *result = bucket->value;    
            return SUCCESS;
        }
 
        bucket = bucket->next;
    }
 
    LOG_MSG("HashTable lookup missed the key: %s\n", key);
    return FAILED;
}

PHP中的数组是基于哈希表实现的,依次给数组添加元素时,元素之间是有顺序的,而这里的哈希表在物理上显然是接近平均分布的,这样是无法根据插入的先后顺序获取到这些元素的,在PHP的实现中Bucket结构体还维护了另一个指针字段来维护元素之间的关系。

 

二、PHP的哈希表实现

    1、PHP的哈希实现

    PHP中的哈希表是十分重要的一个数据接口,基本上大部分的语言特征都是基于哈希表的,例如:变量的作用域和变量的存储,类的实现以及Zend引擎内部的数据有很多都是保存在哈希表中的。

    (1)数据结构及说明

    Zend为了保存数据之间的关系使用了双向链表来保存数据

    (2)哈希表结构

    PHP中的哈希表实现在Zend/zend_hash.c中,PHP使用如下两个数据结构来实现哈希表,HashTable结构体用于保存整个哈希表需要的基本信息,而Bucket结构体用于保存具体的数据内容,如下:

typedef struct _hashtable { 
    uint nTableSize;        // hash Bucket的大小,最小为8,以2x增长
    uint nTableMask;        // nTableSize-1,索引取值的优化
    uint nNumOfElements;    // hash Bucket中当前存在的元素个数,count()函数会直接返回此值
    ulong nNextFreeElement; // 下一个数字索引的位置
    Bucket *pInternalPointer;   // 当前遍历的指针(foreach 比for快的原因之一)
    Bucket *pListHead;          // 存储数头元素指针
    Bucket *pListTail;          // 存储数组尾元素指针
    Bucket **arBuckets;         // 存储hash数组
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
    zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3此
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

    nTableSize字段用于标示哈希表的容量,哈希表的初始化容量最小为8.首先看看哈希表的初始化函数:

ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t 
pHashFunction,
                    dtor_func_t pDestructor, zend_bool persistent 
ZEND_FILE_LINE_DC)
{
    uint i = 3;
    //...
    if (nSize >= 0x80000000) {
        /* prevent overflow */
        ht->nTableSize = 0x80000000;
    } else {
        while ((1U << i) < nSize) {
            i++;
        }
        ht->nTableSize = 1 << i;
    }
    // ...
    ht->nTableMask = ht->nTableSize - 1;
 
    /* Uses ecalloc() so that Bucket* == NULL */
    if (persistent) {
        tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *));
        if (!tmp) {
            return FAILURE;
        }
        ht->arBuckets = tmp;
    } else {
        tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *));
        if (tmp) {
            ht->arBuckets = tmp;
        }
    }
 
    return SUCCESS;
}

    例如如果设置初始大小为10,则上面的算法将会将大小调整为16.也就是始终将大小调整为接近初始大小的2的整数次方

为什么这么调整呢?先看看HashTable将哈希值映射到槽位的方法:

h = zend_inline_hash_func(arKey, nKeyLength);
nIndex = h & ht->nTableMask;

    从上边的_zend_hash_init()函数中可知,ht->nTableMask的大小为ht->nTableSize – 1。这里使用&操作而不是使用取模,这是因为相对来说取模的操作的消耗和按位与的操作大很多。

    设置好了哈希表的大小后就需要为哈希表申请存储空间了,如上边初始化的代码,根据是否需要持久保存而调用了不同的内存申请方法,是需要持久体现的是在前面PHP生命周期里介绍的:持久内容能在多个请求之间可访问,而如果是非持久存储则会在在请求结束时释放占用的空间。具体内容将在内存管理中详解

    HashTable中的nNumOfElements字段很好理解,每插入一个元素或者unset删掉元素时会更新这个字段,这样在进行count()函数统计数组元素个数时就能快速的返回。

    nNextFreeElement字段非常有用,先看一段PHP代码:

 'Hello');
$a[] = 'TIPI';
var_dump($a);
 
// ouput
array(2) {
  [10]=>
  string(5) "Hello"
  [11]=>
  string(5) "TIPI"
}

    PHP中可以不指定索引值向数组中添加元素,这时将默认使用数字作为索引,和C语言中的枚举类似,而这个元素的索引到底是多个就由nNextFreeElement字段决定了。如果数组中存在了数字key,则会默认使用最新使用的key+1,如上例中已经存在了10作为key的元素,这样新插入的默认索引就为11了。

    下面看看保存哈希表数据的槽位数据结构体:

typedef struct bucket {
    ulong h;            // 对char *key进行hash后的值,或者是用户指定的数字索引值
    uint nKeyLength;    // hash关键字的长度,如果数组索引为数字,此值为0
    void *pData;        // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
    void *pDataPtr;     // 如果是指针数组,此值会指向真正的value,同时上面pData会指向此值
    struct bucket *pListNext;   // 整个hash表的下一个元素
    struct bucket *pListLast;   // 整个hash表的上一个元素
    struct bucket *pNext;       // 存放在同一个hash Bucket内的下一个元素
    struct bucket *pLast;       // 存放在同一个hash Bucket内的上一个元素
    char arKey[1];  
    /*
    存储字符索引,此项必须放在最末尾,因为此处只定义了1个字节,存储的实际上是指向char *key的值,
    这就意味着可以省去再赋值一次的消耗,而且,有时此值并不需要,所以同时还节省了空间。
    */
} Bucket;

    如上面各字段的注释。h字段保存哈希表key哈希后的值。在PHP中可以使用字符串或者数字作为数组的索引。因为数字的索引是唯一的。如果再进行一次哈希将会极大的浪费。h字段后面的nKeyLength字段是作为key长度的标示,如果索引是数字的话,则nKeyLength为0.在PHP中定义数组时如果字符串可以被转换成数字也会进行转换。所以在PHP中例如'10','11'这类的字符索引和数字索引10,11没有区别

  • Bucket结构体维护了两个双向链表,pNext和pLast指针分别指向本槽位所在的链表的关系

  • 而pListNext和pListLast指针指向的则是整个哈希表所有的数据之间的链接关系。HashTable结构体中的pListHead和pListTail则维护整个哈希表的头元素指针和最后一个元素的指针

     

 

    哈希表的操作接口:

    PHP提供了如下几类操作接口:

  • 初始化操作,例如zend_hash_init()函数,用于初始化哈希表接口,分配空间等。

  • 查找,插入,删除和更新操作接口,这是比较常规的操作。

  • 迭代和循环,这类的接口用于循环对哈希表进行操作。

  • 复制,排序,倒置和销毁等操作。

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C++ 设计模式与软件架构
C++ 设计模式与软件架构

本专题深入讲解 C++ 中的常见设计模式与架构优化,包括单例模式、工厂模式、观察者模式、策略模式、命令模式等,结合实际案例展示如何在 C++ 项目中应用这些模式提升代码可维护性与扩展性。通过案例分析,帮助开发者掌握 如何运用设计模式构建高质量的软件架构,提升系统的灵活性与可扩展性。

9

2026.01.30

c++ 字符串格式化
c++ 字符串格式化

本专题整合了c++字符串格式化用法、输出技巧、实践等等内容,阅读专题下面的文章了解更多详细内容。

9

2026.01.30

java 字符串格式化
java 字符串格式化

本专题整合了java如何进行字符串格式化相关教程、使用解析、方法详解等等内容。阅读专题下面的文章了解更多详细教程。

12

2026.01.30

python 字符串格式化
python 字符串格式化

本专题整合了python字符串格式化教程、实践、方法、进阶等等相关内容,阅读专题下面的文章了解更多详细操作。

3

2026.01.30

java入门学习合集
java入门学习合集

本专题整合了java入门学习指南、初学者项目实战、入门到精通等等内容,阅读专题下面的文章了解更多详细学习方法。

20

2026.01.29

java配置环境变量教程合集
java配置环境变量教程合集

本专题整合了java配置环境变量设置、步骤、安装jdk、避免冲突等等相关内容,阅读专题下面的文章了解更多详细操作。

18

2026.01.29

java成品学习网站推荐大全
java成品学习网站推荐大全

本专题整合了java成品网站、在线成品网站源码、源码入口等等相关内容,阅读专题下面的文章了解更多详细推荐内容。

19

2026.01.29

Java字符串处理使用教程合集
Java字符串处理使用教程合集

本专题整合了Java字符串截取、处理、使用、实战等等教程内容,阅读专题下面的文章了解详细操作教程。

3

2026.01.29

Java空对象相关教程合集
Java空对象相关教程合集

本专题整合了Java空对象相关教程,阅读专题下面的文章了解更多详细内容。

6

2026.01.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
c语言项目php解释器源码分析探索
c语言项目php解释器源码分析探索

共7课时 | 0.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号