哈希表(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 都进行了哈希,扩容哈希表不能简单的扩容,而需要重新将原来的预算插入到新的容器中。