哈希表的一些知识点

抛砖引玉,你们有没有想过Object-C中,NSDictionary是如何实现根据key快速查值的?php

前言

在编程的世界中,比较重要的数据结构有如下3个:html

  • struct 结构体
  • array 数组
  • link list 链表

咱们都知道,数组是存储数据的自然载体,在内存中是一段连续的地址,正是因为这个特性,咱们可以经过数组的下标快速的获取与之对应的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)连接法是经过数组和链表组合而成的。当发生冲突的时候只要将其加到对应的链表中便可。

与开放定址法相比,连接法有以下几个优势:

  • 连接法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,所以平均查找长度较短;
  • 因为连接法中各链表上的结点空间是动态申请的,故它更适合于造表前没法肯定表长的状况;
  • 开放定址法为减小冲突,要求装填因子α较小,故当结点规模较大时会浪费不少空间。而连接法中可取α≥1,且结点较大时,拉链法中增长的指针域可忽略不计,所以节省空间;
  • 在用连接法构造的散列表中,删除结点的操做易于实现。只要简单地删去链表上相应的结点便可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,不然将截断在它以后填人散列表的同义词结点的查找路径。这是由于各类开放地址法中,空地址单元(即开放地址)都是查找失败的条件。所以在 用开放地址法处理冲突的散列表上执行删除操做,只能在被删结点上作删除标记,而不能真正删除结点。

固然连接法也有其缺点,拉链法的缺点是:指针须要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可以使装填因子变小,这又减小了开放定址法中的冲突,从而提升平均查找速度。

负载因子 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中正常哈希表和退化哈希表的示意图。

总结

哈希和哈希表的概念是这篇文章的重点,即便开发中用到的几率不高。

参考

  1. PHP哈希表碰撞攻击原理
  2. 谈谈 Hash Table
  3. 深刻理解哈希表
相关文章
相关标签/搜索