抛砖引玉,你们有没有想过Object-C中,NSDictionary是如何实现根据key快速查值的?php
在编程的世界中,比较重要的数据结构有如下3个:html
咱们都知道,数组是存储数据的自然载体,在内存中是一段连续的地址,正是因为这个特性,咱们可以经过数组的下标快速的获取与之对应的value。本质上是经过下标来计算出value的内存地址。算法
NSDictionary自己是一种key-value的结构关系。其内部也是用数组来存储value。那么就产生了一个最核心的问题,如何根据key来获取value的内存地址?这个过程既简单又不简单,由此咱们引出哈希表的知识点。编程
哈希表是一种数据结构,实现key-value的快速存取。以前说过数组能够实现快速存取,因此哈希表确定会使用到数组。在这里,咱们把每个数组的单元叫作一个bucket(桶)。swift
CoreFoundation中的桶:数组
typedef struct { CFIndex idx; uintptr_t weak_key; uintptr_t weak_value; uintptr_t count; } CFBasicHashBucket;
PHP中的桶:数据结构
typedef struct bucket { ulong h; /* Used for numeric indexing */ uint nKeyLength; void *pData; void *pDataPtr; struct bucket *pListNext; struct bucket *pListLast; struct bucket *pNext; struct bucket *pLast; char arKey[1]; /* Must be last element */ } Bucket;
经过上边的结构体,咱们知道了,在数组中存放的是一个一个的桶,那么应该如何经过key来获取到指定的桶呢?函数
这时候就须要一个函数来实现这样一个功能,经过key计算出桶的内存地址。这个函数就叫作哈希函数F(x)。在现实中,考虑到性能的缘由,这个哈希函数不会被设计成完美函数,即一个key只生成一个结果。有可能两个不一样的key1,key2经过哈希函数获得了同一个值,这时候咱们就称key1,key2为同义词,所以就产生了冲突,这个冲突每每发生在插值的过程当中。性能
既然产生了冲突,就要想办法解决冲突。解决冲突的方式主要分两类 开放定址法(Open addressing)这种方法就是在计算一个key的哈希的时候,发现目标地址已经有值了,即发生冲突了,这个时候经过相应的函数在此地址后面的地址去找,直到没有冲突为止。这个方法经常使用的有线性探测,二次探测,再哈希。 这种解决方法有个很差的地方就是,当发生冲突以后,会在以后的地址空间中找一个放进去,这样就有可能后来出现一个key哈希出来的结果也正好是它放进去的这个地址空间,这样就会出现非同义词的两个key发生冲突。ui
连接法(Separate chaining)连接法是经过数组和链表组合而成的。当发生冲突的时候只要将其加到对应的链表中便可。
与开放定址法相比,连接法有以下几个优势:
固然连接法也有其缺点,拉链法的缺点是:指针须要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可以使装填因子变小,这又减小了开放定址法中的冲突,从而提升平均查找速度。
负载因子 Load factor a=哈希表的实际元素数目(n)/ 哈希表的容量(m) a越大,哈希表冲突的几率越大,可是a越接近0,那么哈希表的空间就越浪费。 通常状况下建议Load factor的值为0-0.7,Java实现的HashMap默认的Load factor的值为0.75,当装载因子大于这个值的时候,HashMap会对数组进行扩张至原来两倍大。
不论使用了哪一种碰撞解决策略,都致使插入和查找操做的时间复杂度再也不是O(1)。以查找为例,不能经过key定位到桶就结束,必须还要比较原始key(即未作哈希以前的key)是否相等,若是不相等,则要使用与插入相同的算法继续查找,直到找到匹配的值或确认数据不在哈希表中。
PHP是使用单链表存储碰撞的数据,所以实际上PHP哈希表的平均查找复杂度为O(L),其中L为桶链表的平均长度;而最坏复杂度为O(N),此时全部数据所有碰撞,哈希表退化成单链表。下图PHP中正常哈希表和退化哈希表的示意图。
哈希和哈希表的概念是这篇文章的重点,即便开发中用到的几率不高。