php数组的数据结构
2016-03-01 11:24
525 查看
从zvalue_value结构中看出,php的数组是用哈希表来实现的:HashTable *ht;
引用网上的内容介绍下哈希表:
哈希表是一种通过哈希函数,将特定的键映射到特定值的一种数据结构,它维护键和值之间一一对应关系。
键(key):用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等。
槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。
哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。
哈希冲突(hash collision):哈希函数将两个不同的key映射到同一个索引的情况。
当出现冲突时,php使用链接法来解决冲突,也就是不同的key映射到同一个槽时,这些key将按链表的方式存储,具体解决方法在下文中介绍。
php哈希的实现
php中和哈希实现相关的两个文件是Zend/zend_hash.h和Zend/zend_hash.c。在zend_hash.h中申明了哈希的数据结构和相关操作的函数,包括初始化/释放、哈希函数、基本增删改查操作函数以及为方便操作而提供的类似于utility性质的实用函数。
哈希函数hash function
php采用经典的Daniel J. Bernstein的'times 33'哈希算法,这个算法是已知的最好的哈希算法之一,在处理以字符串为键值的哈希时,有着极快的计算效率和很好哈希分布,具体算法可参考相关文档。
Bucket数据结构
php采用的是链接法解决冲突,所以每个Bucket数据结构中除了指向实际数据的指针,还需要指向链表下一个元素的指针,为了加快查找时间,php的bucket实现中采用双向链表,具体结构如下:
typedef struct bucket {
ulong h; // 对char *key进行hash后的值,或者是数字的索引值
uint nKeyLength; // hash关键字长度,如果索引为数字,那么这个值为0
void *pData; // 指向用户数据(其实是数据副本,在做UPDATE_DATA或者INIT_DATA时分配了新的空间,将用户数据拷贝了一份),如果是一个指针,那么指向下面的pDataPtr
void *pDataPtr; // 如果是指针数据,那么这个值指向真正的用户数据(同上一个,也是用户数据的副本),同时上面的pData指向该值。
struct bucket *pListNext; // 指向整个hash表的下一个元素
struct bucket *pListLast; // 指向整个hash表的上一个元素
struct bucket *pNext; // 指向同一个Bucket内的下一个元素
struct bucket *pLast; // 指向同一个Bucket内的上一个元素
char arKey[1];
} Bucket;
这个结构中有两套双向链表指针:pListNext&pListLast,pNext&pLast。其中,pListNext和pListLast用于构成整个哈希表的链表,这个链表中包括哈希表中的所有元素,按插入顺序构成链表。而pNext和pLast则构成同一个槽位内的Bucket链表,用于解决冲突。
拿一个具体的例子来看php哈希的内部指针,如图中所示(该图来自于网络),先后插入Bucket1,Bucket2和Bucket3,同时Bucket1和Bucket2冲突:
这种结构中的几个关键点:
1. 初始化时哈希表的大小:在初始化函数_zend_hash_init中,哈希表的实际大小被初始化为2的整数倍,这样做的好处是,哈希表的可用索引值为0 - (2^n-1),当需要计算某个哈希值在哈希表中的具体位置时,直接用与方法:nIndex = h & ht->nTableMask;其中,nTableMash = nTableSize-1;按位与操作相比取模操作快很多。
2. 遍历哈希表:在HashTable数据结构中,有这么一个成员:pInternalPointer。这个变量指向当前遍历的元素,所以可以快速的取到下一个元素(通过pListNext取得整个哈希表中的下一个,pListLast取得整个哈希表中的上一个)。所以执行foreach语句比执行for语句快。
3. 纯数字字符串key的索引:如果key是一个数值,那么php不会去计算这个key的哈希值,直接拿这个数值作为哈希值(如果是一个负数,会转成ulong),如果key是一个纯数字组成的字符串,php的实际做法是调用ZEND_HANDLE_NUMERIC宏,在这个宏中,会将字符串转成数值,然后将这个数值作为哈希值,所以对于纯数字的字符串key和相应数值的key没有区别,比如'10'和10这两个key是一样的。
4. 一个新的Bucket插入到哈希表中的两个操作,一个是插入到某个槽位上的List:CONNECT_TO_BUCKET_DLLIST宏,另一个是插入到哈希表的整个链表中:CONNET_TO_GLOBAL_DLLIST宏。
5. resize操作:在宏ZEND_HASH_IF_FULL_DO_RESIZE中检查是否需要resize,当元素个数超过哈希表的大小时,就做resize操作。resize时,直接将nTableSize左移1位,然后将已有的元素重新hash。
6. 实用函数:php的哈希中提供了很多实用的函数,包括copy,merge,sort(如果指定了renumbre参数,那么key会重建),minmax,方便遍历的forward和backward等。
另外:hash解决冲突的几种算法:
1)线性探查法(Linear Probing)
该方法的基本思想是:
将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
利用开放地址法的一般形式,线性探查法的探查序列为:
hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
(2)线性补偿探测法
线性补偿探测法的基本思想是:
将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j + Q) % m ,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m = 1321 ,选用 Q = 25 。
(3)随机探测
随机探测的基本思想是:
将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
(4)拉链法解决冲突的方法
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
【例】设有 m = 5 , H(K) = K mod 5 ,关键字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外链地址法所建立的哈希表如下图所示:
引用网上的内容介绍下哈希表:
哈希表是一种通过哈希函数,将特定的键映射到特定值的一种数据结构,它维护键和值之间一一对应关系。
键(key):用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等。
槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。
哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。
哈希冲突(hash collision):哈希函数将两个不同的key映射到同一个索引的情况。
当出现冲突时,php使用链接法来解决冲突,也就是不同的key映射到同一个槽时,这些key将按链表的方式存储,具体解决方法在下文中介绍。
php哈希的实现
php中和哈希实现相关的两个文件是Zend/zend_hash.h和Zend/zend_hash.c。在zend_hash.h中申明了哈希的数据结构和相关操作的函数,包括初始化/释放、哈希函数、基本增删改查操作函数以及为方便操作而提供的类似于utility性质的实用函数。
哈希函数hash function
php采用经典的Daniel J. Bernstein的'times 33'哈希算法,这个算法是已知的最好的哈希算法之一,在处理以字符串为键值的哈希时,有着极快的计算效率和很好哈希分布,具体算法可参考相关文档。
Bucket数据结构
php采用的是链接法解决冲突,所以每个Bucket数据结构中除了指向实际数据的指针,还需要指向链表下一个元素的指针,为了加快查找时间,php的bucket实现中采用双向链表,具体结构如下:
typedef struct bucket {
ulong h; // 对char *key进行hash后的值,或者是数字的索引值
uint nKeyLength; // hash关键字长度,如果索引为数字,那么这个值为0
void *pData; // 指向用户数据(其实是数据副本,在做UPDATE_DATA或者INIT_DATA时分配了新的空间,将用户数据拷贝了一份),如果是一个指针,那么指向下面的pDataPtr
void *pDataPtr; // 如果是指针数据,那么这个值指向真正的用户数据(同上一个,也是用户数据的副本),同时上面的pData指向该值。
struct bucket *pListNext; // 指向整个hash表的下一个元素
struct bucket *pListLast; // 指向整个hash表的上一个元素
struct bucket *pNext; // 指向同一个Bucket内的下一个元素
struct bucket *pLast; // 指向同一个Bucket内的上一个元素
char arKey[1];
} Bucket;
这个结构中有两套双向链表指针:pListNext&pListLast,pNext&pLast。其中,pListNext和pListLast用于构成整个哈希表的链表,这个链表中包括哈希表中的所有元素,按插入顺序构成链表。而pNext和pLast则构成同一个槽位内的Bucket链表,用于解决冲突。
拿一个具体的例子来看php哈希的内部指针,如图中所示(该图来自于网络),先后插入Bucket1,Bucket2和Bucket3,同时Bucket1和Bucket2冲突:
这种结构中的几个关键点:
1. 初始化时哈希表的大小:在初始化函数_zend_hash_init中,哈希表的实际大小被初始化为2的整数倍,这样做的好处是,哈希表的可用索引值为0 - (2^n-1),当需要计算某个哈希值在哈希表中的具体位置时,直接用与方法:nIndex = h & ht->nTableMask;其中,nTableMash = nTableSize-1;按位与操作相比取模操作快很多。
2. 遍历哈希表:在HashTable数据结构中,有这么一个成员:pInternalPointer。这个变量指向当前遍历的元素,所以可以快速的取到下一个元素(通过pListNext取得整个哈希表中的下一个,pListLast取得整个哈希表中的上一个)。所以执行foreach语句比执行for语句快。
3. 纯数字字符串key的索引:如果key是一个数值,那么php不会去计算这个key的哈希值,直接拿这个数值作为哈希值(如果是一个负数,会转成ulong),如果key是一个纯数字组成的字符串,php的实际做法是调用ZEND_HANDLE_NUMERIC宏,在这个宏中,会将字符串转成数值,然后将这个数值作为哈希值,所以对于纯数字的字符串key和相应数值的key没有区别,比如'10'和10这两个key是一样的。
4. 一个新的Bucket插入到哈希表中的两个操作,一个是插入到某个槽位上的List:CONNECT_TO_BUCKET_DLLIST宏,另一个是插入到哈希表的整个链表中:CONNET_TO_GLOBAL_DLLIST宏。
5. resize操作:在宏ZEND_HASH_IF_FULL_DO_RESIZE中检查是否需要resize,当元素个数超过哈希表的大小时,就做resize操作。resize时,直接将nTableSize左移1位,然后将已有的元素重新hash。
6. 实用函数:php的哈希中提供了很多实用的函数,包括copy,merge,sort(如果指定了renumbre参数,那么key会重建),minmax,方便遍历的forward和backward等。
另外:hash解决冲突的几种算法:
1)线性探查法(Linear Probing)
该方法的基本思想是:
将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
利用开放地址法的一般形式,线性探查法的探查序列为:
hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
(2)线性补偿探测法
线性补偿探测法的基本思想是:
将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j + Q) % m ,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m = 1321 ,选用 Q = 25 。
(3)随机探测
随机探测的基本思想是:
将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
(4)拉链法解决冲突的方法
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
【例】设有 m = 5 , H(K) = K mod 5 ,关键字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外链地址法所建立的哈希表如下图所示:
相关文章推荐
- IOS底层数据结构--class
- 数据结构(14)--线索二叉树的实现
- 数据结构——算法之(041)(寻找数组中的最大值和最小值)
- python,数据结构
- POJ3580[memo] 旋转吧,splay!
- java 数据结构和存储方式
- 2015年大二上-数据结构-图-2-(2)-Kruskal算法
- 数组和广义表-第5章-《数据结构题集》习题解析-严蔚敏吴伟民版
- 5-6-广义表(扩展线性链表存储表示)-数组和广义表-第5章-《数据结构》课本源码-严蔚敏吴伟民版
- 5-5-广义表(头尾链表存储表示)-数组和广义表-第5章-《数据结构》课本源码-严蔚敏吴伟民版
- java递归算法实现
- 数据结构-阶段性理解
- 数据结构与算法小结(2)
- C#数据结构杂记
- R语言学习:数据结构8-日期和时间
- R语言学习:数据结构7-数据框
- R语言学习:数据结构6-缺失值
- R语言学习:数据结构5-因子
- R语言学习:数据结构4-列表
- R语言学习:数据结构3-矩阵、数组