[李景山php] 深入理解PHP内核[读书笔记]--第三章:变量及数据类型--变量的结构和类型--HashTable

49 篇文章 0 订阅

哈希表(Hash Table)


PHP用的最多的是字符串及数组,PHP容易上手也得益于灵活的数组类型。
哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,通常哈希表的这些操作时间复杂度为O(1)。

基本概念


哈希表是一种通过哈希函数,将特定的键映射到特定的值的一种数据结构,它维护键和值之间一一对应关系。

键(key):用于操作数据的表示,例如PHP数组中的索引,或者字符串键等等。
槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。
哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。
哈希冲突(hash collision):哈希函数将两个不同的key映射到同一个索引的情况。

哈希表可以理解为数组的扩展或者关联数组,数组用数字下标来寻址,如果关键字(key)的范围较小且是数字的话,我们可以直接使用数组来完成哈希表,而如果关键字范围太大,如果直接使用数组我们需要为所有可能的key申请空间。

很多情况会出现这样的情况:
空间足够,就会出现空间利用率比较低。

通过合理设计的哈希函数,我们就能将key映射到合适的范围,如果不同的key指向同一个value的时候,就会出现哈希冲突。

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

冲突解决(hash 冲突解决)


连接法

连接法通过使用一个链表来保存slot值的方式来解决冲突,也就是当不同的key映射到一个槽中的时候用链表来保存这些值。所以使用连接法是在最坏的情况下,也就是所有的key都映射到同一个槽中了,这样哈希表就退化成了一个链表,这样的话操作链表的时间复杂度则成了O(n),这样哈希表的性能优势就没有了,所以选择一个合适的哈希函数是最为关键的。

如果哈希表退化成为连表,造成性能极具下降。

哈希冲突攻击利用的哈希表最根本的弱点是:开源算法和哈希实现的确定性以及可预测性,这样攻击者可以利用特殊构造的key来进行攻击。要解决这个问题的方法则是让攻击者无法轻易构造能够进行攻击的key序列。

总结:漏洞的原因就是攻击者知道出牌规则。
延伸:如果这样的话,攻击者可以通过构造一定的数组数据,造成链表性能瘫痪。

PHP的防止方法:限制用户提交数据字段数量。
但是攻击者可以使用 json_decode()进行攻击。

开放寻址法

开放寻址法:使用开放寻址法是槽本身直接存放数据,在插入数据时如果key所映射到的索引已经有数据了,这说明发生了冲突,这是会寻找一下槽,如果该槽也被占用了则继续寻找下一个槽,直接寻找到没有被占用的槽,在查找时也使用同样的策略来进行。

但是这样会出现新的问题:就是后续插入的数据需要更多此的查找新的位置,导致性能下降。

装载因子是哈希表保存的元素数量和哈希表容量的比,开放寻址最好不要大于0.5

哈希表的实现


实现哈希表的主要需要完成三个问题:
1 实现哈希函数
2 冲突的解决
3 操作接口的实现

数据结构


首先我们需要一个容器来保存我们的哈希表,哈希表需要保存的内容主要是要保存进来的数据,同时为了方便的得知哈希表中存储的元素个数,需要保存一个大小字段,第二个需要的就是保存数据的容器了。

作为实例,下面将实现一个简易的哈希表,基本的数据结构主要有两个,一个用于保存哈希表本身,另外一个就是用于实际保存数据的单链表了,定义如下:

// 存数据  单链表
typedef struct _Bucket
{
     char *key;
     void *value;
     struct _Bucket *next;
}Bucket;
// 存结构
typedef struct _HashTable
{
     int size;
     int elem_num;
     Bucket** buckets;
}HashTable;

上面的定义和PHP中的实现类似,为了便于理解裁剪了大部分无关的细节,在本节中为了简化,key的数据类型为字符串,而存储的数据类型可以为任意类型。

Bucket结构体是一个单链表,这是为了解决多个key哈希冲突的问题,也就是前面所提到的连接法,当多个key映射到同一个key的时候将冲突的元素连接起来。

哈希函数实现


哈希函数需要尽可能的将不同的key映射到不同的槽(slot或者bucket)中,首先我们采用一种最为简单的哈希算法实现,将key字符串的所有字符串加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了。

static int has_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 has_init(HashTable *ht);//初始化哈希表
int has_lookup(HashTable *ht,char *key,void **result);//根据key查找内容
int has_insert(HashTable *ht,char *key,void *value);//将内容插入到哈希表中
int hash_remove(HashTable *ht,char *key);//删除key所指向的内容
int hash_destory(HashTable *ht);

具体的实现:

int hash_init(HashTable *ht)
{
     ht->size     = HASH_TABLE_INIT_SIZE;
     ht->elem_num     = 0;
     ht->bukets     = (Bucket **)calloc(ht->size,sizeof(Bucket *));

     if(ht->buckets == NULL) return FAILED;
     LOG_MSG("[init]\tsize %i\n",ht->size);
     return SUCCESS;
}

初始化的主要工作是为哈希表申请存储空间,函数使用calloc函数的目的是确保数据存储的槽都初始化为0,以便后续在插入和查找时确认该槽是否被占用。

备注:就是各种解决方案。

int hash_insert(HashTable *ht,char *key,void *value)
{
     //check if we need to resize the hashtable
     resize_hash_table_if_need(ht);
     int index = HASH_INDEX(ht,key);
     Bucket *org_bucket = ht->buckets[index];
     Bucket *tmp_bucket = org_bucket;

     //check if the key exists already
     while(tmp_bucket)
     {
         if(strcmp(key,tmp_bucket->key) == 0)
          {
               LOG_MSG("[update]\tkey:%s\n",key);
      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->elem_num +=1;

 if(org_bucket != NULL)
 {
      LOG_MSG("[collision]\tindex:%d key: %s \n",index, key);
      bucket->next = org_bucket; 
 }
 ht->buckets[index] = bucket;
 LOG_MSG("[insert]\tindex:%d key: %s \tht(num %d) \n", index,key,ht->elem_num);

 return SUCESS;

}

上面这个哈希表的插入操作比较简单,简单的以key做哈希,找到元素应该存储的位置,并检查该位置是否已经有了内容,如果发生碰撞则将新元素连接到原有元素连接表头部。

由于在插入过程中可能会导致哈希表的元素个数比较多,如果超过了哈希表的容量,则说明肯定会出现碰撞,出现碰撞则会导致哈希表的性能下降,为此如果出现元素容量达到容量则需要进行扩容,由于所有的key 都进行了哈希,扩容哈希表不能简单的扩容,而需要重新将原来的预算插入到新的容器中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值