原文连接:http://www.orlion.ga/241/php
1、哈希表(HashTable)算法
大部分动态语言的实现中都使用了哈希表,哈希表是一种经过哈希函数,将特定的键映射到特定值得一种数据数组
结构,它维护键和值之间一一对应关系。数据结构
键(key):用于操做数据的标示,例如PHP数组中的索引或者字符串键等等。函数
槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数组真正存放的容器。优化
哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。ui
哈希冲突(hash collision):哈希函数将两个不一样的key映射到同一个索引的状况。指针
目前解决hash冲突的方法有两种:连接法和开放寻址法。排序
一、冲突解决递归
(1)连接法
连接法经过使用一个链表来保存slot值的方式来解决冲突,也就是当不一样的key映射到一个槽中的时候使用链表
来保存这些值。(PHP中正是使用了这种方式);
(2)开放寻址法
使用开放寻址法是槽自己直接存放数据,在插入数据时若是key所映射到的索引已经有数据了,这说明有冲突,
这时会寻找下一个槽,若是该槽也被占用了则继续寻找下一个槽,直到找到没有被占用的槽,在查找时也是这样
二、哈希表的实现
哈希表的实现主要完成的工做只有三点:
* 实现哈希函数
* 冲突的解决
* 操做接口的实现
(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结构体还维护了另外一个指针字段来维护元素之间的关系。
2、PHP的哈希表实现
一、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()函数,用于初始化哈希表接口,分配空间等。
查找,插入,删除和更新操做接口,这是比较常规的操做。
迭代和循环,这类的接口用于循环对哈希表进行操做。
复制,排序,倒置和销毁等操做。