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

一、哈希表(HashTable)

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

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

键(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字符串的所有字符加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了。

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代码:

<?php
$a = array(10 => '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的哈希表实现的更多相关文章

  1. 深入理解PHP内核(六)函数的定义、传参及返回值

    一.函数的定义 用户函数的定义从function 关键字开始,如下 function foo($var) { echo $var; } 1.词法分析 在Zend/zend_language_scann ...

  2. 深入理解PHP内核(九)变量及数据类型-静态变量

    原文链接:http://www.orlion.ga/251/ 通常静态变量是静态分配的,他们的生命周期和程序的生命周期一样长,只有在程序退出后才结束生命周期,这和局部变量相反,有的语言中全局变量也是静 ...

  3. 深入理解PHP内核(二)概览-PHP生命周期与Zend引擎

    本文参考自<深入理解PHP内核>,地址:https://github.com/reeze/tipi 本文链接:http://www.orlion.ml/232/ 1.SAPI接口 SAPI ...

  4. 读书笔记之Linux系统编程与深入理解Linux内核

    前言 本人再看深入理解Linux内核的时候发现比较难懂,看了Linux系统编程一说后,觉得Linux系统编程还是简单易懂些,并且两本书都是讲Linux比较底层的东西,只不过侧重点不同,本文就以Linu ...

  5. 深入理解php内核

    目录 第一部分 基本原理 第一章 准备工作和背景知识 第一节 环境搭建 第二节 源码布局及阅读方法 第三节 常用代码 第四节 小结 第二章 用户代码的执行 第一节 PHP生命周期 第二节 从SAPI开 ...

  6. 【读书笔记::深入理解linux内核】内存寻址【转】

    转自:http://www.cnblogs.com/likeyiyy/p/3837272.html 我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0 ...

  7. 【读书笔记::深入理解linux内核】内存寻址

    我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0xC0000000:这是内核地址空间的地址转换关系. 这句话瞬间让我惊呆了,根据我的CPU的知识,开 ...

  8. 《深入理解Linux内核》 读书笔记

    深入理解Linux内核 读书笔记 一.概论 操作系统基本概念 多用户系统 允许多个用户登录系统,不同用户之间的有私有的空间 用户和组 每个用于属于一个组,组的权限和其他人的权限,和拥有者的权限不一样. ...

  9. 推荐一本书《深入理解PHP内核》

    <深入理解PHP内核> 在线网址:http://www.php-internals.com/

随机推荐

  1. mac命令行常用

    1. 寻找文件命令 find . -name "*.txt" //.代表当前路径,意思是找到所有txt文件 2. ps -A | grep mysql //可以检查到mysql的进 ...

  2. 洛谷P1330 封锁阳光大学

    题目描述 曹是一只爱刷街的老曹,暑假期间,他每天都欢快地在阳光大学的校园里刷街.河蟹看到欢快的曹,感到不爽.河蟹决定封锁阳光大学,不让曹刷街. 阳光大学的校园是一张由N个点构成的无向图,N个点之间由M ...

  3. 一些IOS开发中的小技巧

    1.打包后提交报错误 错误信息:ERROR ITMS-90035: "Invalid Signature. Code object is not signed at all. The bin ...

  4. ubuntu安装hexo博客

    ubuntu下安装hexo博客 一 安装git sudo apt-get install git 二 安装nodejs 官网下载linux安装包.tar.gz文件 解压 tar zxvf 这样变可以切 ...

  5. petapoco 使用 MiniProfiler Glimpse监控

    PetaPoco是一款适用于.Net(window) 和Mono( linux )的微小.快速.单文件的微型ORM. MVC MiniProfiler是Stack Overflow团队设计的一款对AS ...

  6. mysql大数据表改表结构方案

    有一个表有上千W数据, 用什么方法给这个表加一个字段最快?1. alert2. 建一个表和第一个表一样,只是多了要加的字段,然后用多个INSERT INTO SELECT语句limit写入3. 就是导 ...

  7. 多线程_先产后销_运行结果有BUG

    class Shop { public static void main(String[] args) { Things t=new Things(); Custom c=new Custom(t); ...

  8. [转]Oracle VM VirtualBox虚拟机,Ubuntu虚拟机共享文件夹

    VirtualBox的菜单里选择"设备" -> "安装增强功能...". "设备" -> "共享文档夹",添 ...

  9. zookeeper 的多线程和单线程库使用对比

    zookeeper提供了两个库,zookeeper_st和 zookeeper_mt. 前者是单线程库,仅仅提供了异步API和集成在应用程序实现循环中的回调函数,这个库是为了支持pthread库不支持 ...

  10. node.js初学遇到的问题

    是用express安装一个网站基础架构时 express -t ejs microblog 但是出来的模板引擎是jade,通过修改js也修改模板引用npm install 等等修改了index.ejs ...