最近看PHP数组底层结构,用到了哈希表,因此仍是老老实实回去看结构,在这里去总结一下。
这里先说一下哈希(hash)表的定义:哈希表是一种根据关键码去寻找值的数据映射结构,该结构经过把关键码映射的位置去寻找存放值的地方,提及来可能感受有点复杂,我想我举个例子你就会明白了,最典型的的例子就是字典,你们估计小学的时候也用过很多新华字典吧,若是我想要获取“按”字详细信息,我确定会去根据拼音an去查找 拼音索引(固然也能够是偏旁索引),咱们首先去查an在字典的位置,查了一下获得“安”,结果以下。这过程就是键码映射,在公式里面,就是经过key去查找f(key)。其中,按就是关键字(key),f()就是字典索引,也就是哈希函数,查到的页码4就是哈希值。
node
经过字典查询数据算法
可是问题又来了,咱们要查的是“按”,而不是“安,可是他们的拼音都是同样的。也就是经过关键字按和关键字安能够映射到同样的字典页码4的位置,这就是哈希冲突(也叫哈希碰撞),在公式上表达就是key1≠key2,但f(key1)=f(key2)。冲突会给查找带来麻烦,你想一想,你原本查找的是“按”,可是却找到“安”字,你又得向后翻一两页,在计算机里面也是同样道理的。数组
但哈希冲突是无可避免的,为何这么说呢,由于你若是要彻底避开这种状况,你只能每一个字典去新开一个页,而后每一个字在索引里面都有对应的页码,这就能够避免冲突。可是会致使空间增大(每一个字都有一页)。函数
既然没法避免,就只能尽可能减小冲突带来的损失,而一个好的哈希函数须要有如下特色:性能
1.尽可能使关键字对应的记录均匀分配在哈希表里面(好比说某厂商卖30栋房子,均匀划分ABC3个区域,若是你划分A区域1个房子,B区域1个房子,C区域28个房子,有人来查找C区域的某个房子最坏的状况就是要找28次)。ui
2.关键字极小的变化能够引发哈希值极大的变化。spa
比较好的哈希函数是time33算法。PHP的数组就是把这个做为哈希函数。指针
核心的算法就是以下:code
unsigned long hash(const char* key){ unsigned long hash=0; for(int i=0;i<strlen(key);i++){ hash = hash*33+str[i]; } return hash; }
若是遇到冲突,哈希表通常是怎么解决的呢?具体方法有不少,百度也会有一堆,最经常使用的就是开发定址法和链地址法。blog
1.开发定址法
若是遇到冲突的时候怎么办呢?就找hash表剩下空余的空间,找到空余的空间而后插入。就像你去商店买东西,发现东西卖光了,怎么办呢?找下一家有东西卖的商家买呗。
因为我没有深刻试验过,因此贴上在书上的解释:
2.链地址法
上面所说的开发定址法的原理是遇到冲突的时候查找顺着原来哈希地址查找下一个空闲地址而后插入,可是也有一个问题就是若是空间不足,那他没法处理冲突也没法插入数据,所以须要装填因子(插入数据/空间)<=1。
那有没有一种方法能够解决这种问题呢?链地址法能够,链地址法的原理时若是遇到冲突,他就会在原地址新建一个空间,而后以链表结点的形式插入到该空间。我感受业界上用的最多的就是链地址法。下面从百度上截取来一张图片,能够很清晰明了反应下面的结构。好比说我有一堆数据{1,12,26,337,353...},而个人哈希算法是H(key)=key mod 16,第一个数据1的哈希值f(1)=1,插入到1结点的后面,第二个数据12的哈希值f(12)=12,插入到12结点,第三个数据26的哈希值f(26)=10,插入到10结点后面,第4个数据337,计算获得哈希值是1,遇到冲突,可是依然只须要找到该1结点的最后链结点插入便可,同理353。
哈希表的拉链法实现
下面解析一下如何用C++实现链地址法。
第一步。
确定是构建哈希表。
首先定义链结点,以结构体Node展现,其中Node有三个属性,一个是key值,一个value值,还有一个是做为链表的指针。还有做为类的哈希表。
#define HASHSIZE 10 typedef unsigned int uint; typedef struct Node{ const char* key; const char* value; Node *next; }Node; class HashTable{ private: Node* node[HASHSIZE]; public: HashTable(); uint hash(const char* key); Node* lookup(const char* key); bool install(const char* key,const char* value); const char* get(const char* key); void display(); };
而后定义哈希表的构造方法
HashTable::HashTable(){ for (int i = 0; i < HASHSIZE; ++i) { node[i] = NULL; } }
第二步。
定义哈希表的Hash算法,在这里我使用time33算法。
uint HashTable::hash(const char* key){ uint hash=0; for (; *key; ++key) { hash=hash*33+*key; } return hash%HASHSIZE; }
第三步。
定义一个查找根据key查找结点的方法,首先是用Hash函数计算头地址,而后根据头地址向下一个个去查找结点,若是结点的key和查找的key值相同,则匹配成功。
Node* HashTable::lookup(const char* key){ Node *np; uint index; index = hash(key); for(np=node[index];np;np=np->next){ if(!strcmp(key,np->key)) return np; } return NULL; }
第四步。
定义一个插入结点的方法,首先是查看该key值的结点是否存在,若是存在则更改value值就好,若是不存在,则插入新结点。
bool HashTable::install(const char* key,const char* value){ uint index; Node *np; if(!(np=lookup(key))){ index = hash(key); np = (Node*)malloc(sizeof(Node)); if(!np) return false; np->key=key; np->next = node[index]; node[index] = np; } np->value=value; return true; }
因为哈希表高效的特性,查找或者插入的状况在大多数状况下能够达到O(1),时间主要花在计算hash上,固然也有最坏的状况就是hash值全都映射到同一个地址上,这样哈希表就会退化成链表,查找的时间复杂度变成O(n),可是这种状况比较少,只要不要把hash计算的公式外漏出去而且有人故意攻击(用兴趣的人能够搜一下基于哈希冲突的拒绝服务攻击),通常也不会出现这种状况。
哈希冲突攻击致使退化成链表
最后附上完整代码下载地址。 点此下 载 源码