iOS中的哈希表

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS中的哈希表相关的知识,希望对你有一定的参考价值。

参考技术A 哈希表也叫散列表,是根据键值(Key value)而直接进行访问的数据结构。也就是说,它通过把键(Key)映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,映射函数得出的值叫哈希值或哈希码,存放记录的数组叫做哈希表。哈希表中存放数据占总空间比例叫做装填因子(负载因子)。

哈希函数的设计要达到输出值的平均分布化,这样能尽可能降低哈希冲突的概率。

一般情况下,哈希函数中输入值与输出值具有 N对1 关系。

哈希函数具有的特点:如果两个哈希码不同,则它们的键(Key)也一定不同。

该特点常用于优化比较两个对象是否相同,先判断hash码是否相同,若hash码不同则不需要进行对象比较。

哈希表优点:查找效率高,插入删除和查找时间复杂度为O(1)。

哈希表缺点:占用空间大,需要预留一定空间,否则哈希冲突概率会很大。哈希表无法记录插入顺序。

哈希函数因为会N对1的关系,会触发哈希冲突。哈希冲突常见解决方式有有以下几种:

1. 开放寻址法

线性探测(偏移1,2,3..)

可能会导致数据堆积一起,降低插入删除和查找效率。

二次方探测(偏移1*1,2*2,3*3..)

相比线性探测不容易造成数据堆积,但当装填因子比较大时,可能会造成一次查询/插入中,同一位置多次被探测。

2. 拉链法

使用数组+链表解决哈希冲突问题。

开放寻址法缺点:

开放寻址法的缺点是在删除元素时,不能真的删除,否则会引起查找失败。需要在删除元素位置做个标记,代表该索引位置可以插入,但是搜索路过时不能中断搜索。

另一方面,开放寻址法因为在冲突时会占用其它哈希索引空间,所以扩容时装填因子不能太大。

哈希表扩容与缩容

当装填因子过大,会增大哈希冲突概率,需要对哈希表进行扩容,因此哈希表一般使用二级指针实现(ios底层使用DisguisedPtr<void *>)。扩容时需要对所有数据进行重新映射(重哈希),一般每次空间*2。

当装填因子太小,且空间占用比较大,比如:if (装填因子 < 0.1 && num >1024) 缩容。

哈希算法本质是一类安全性较高的哈希函数。Hash算法还具有一个特点,就是很难找到逆向规律。

哈希算法常用于密码安全领域,哈希算法中输入值与输出值具有 N对1 关系,常见的哈希算法包括MD5,SHA,CRC16,CRC32。

SideTables是个全局变量,是StripedMap(哈希表)类型。

StripedMap重载了[],可通过下标的方式快速存取。

StripedMap事实上不是个常规的哈希表,我认为它可以叫做只读哈希表。它在创建之初就会以模版类型初始化满所有存储空间,且不支持修改和扩容。因此,模版类型传递指针是没有意义的。一般模版都设置为结构体,后续操作针对该结构体改值。

SideTables的哈希Key是对象地址addr,哈希函数是((addr >>4) ^ (addr >>9)) %StripeCount,Value是SideTable结构体。因此,SideTables存储着对象地址与SideTable的 N-1 关系。这样可以把对象信息分散到不同SideTable中,提升查找效率。

每个SideTable中存储一堆对象的弱引用表和引用计数表。

weak_table_t的哈希Key是对象地址,哈希函数如下:

Value是weak_entry_t。weak_table_t可以通过对象指针快速找到对象存储的weak_entry_t。

weak_table_t可进行扩容与缩容。

当出现哈希冲突时,与weak_table_t配套的函数使用线性探测法查找。

每个weak_table_t中存储一堆对象的弱引用表。

weak_entry_t只有当装载数据超过一定长度才会启用哈希表存储功能,out_of_line()返回是否启用哈希表。weak_entry_t的哈希Key是弱指针地址,value也是弱指针地址。哈希函数与weak_table_t相同。

weak_entry_t可进行扩容,不会进行缩容。

当出现哈希冲突时,与weak_entry_t配套的函数使用线性探测法查找。

每个weak_entry_t存储指向一个对象的所有弱指针地址(二级指针)。

RefcountMap本质是DenseMap类按照指定模版typedef的类型。

RefcountMap的哈希Key是对象地址,Value是引用计数。

当value为0时,RefcountMap会自动清除这条记录。

当出现哈希冲突时,RefcountMap使用二次方探测法查找。

每个RefcountMap中存储一堆对象的引用计数。

sDataLists是存储所有@synchronized锁的全局哈希表,key是@synchronized的参数。

PropertyLocks是存储所有atomic锁的全局哈希表,key是属性生成的成员变量地址。

类对象的方法缓存cache_t使用哈希表存储bucket_t(sel+imp),使用逆序线性探测(Index-1)解决哈希冲突。

cache_t扩容时会将所有方法缓存清空。

static objc::ExplicitInitDenseSet<const char *> namedSelectors;

namedSelectors是个全局变量,存储所有的方法名SEL,内部结构是哈希表DenseMap。

typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

AssociationsHashMap是AssociationsManager中存储所有关联对象的哈希表,内部结构是DenseMap。

NSObject默认的hash方法是对象地址,即默认没有做优化。

还有个方法isEqual:方法,NSObject默认实现返回该对象与参数对象指针地址是否相同。

hash方法的本质是该对象的哈希函数,需要子类去实现。hash方法主要有两个作用,这两个作用在NSDictionary和NSSet中体现出来。

1. 与isEqual:配合,优化比较

先调用hash方法进行比较,若值相同才调用isEqualTo:进行比较。这依赖于hash的 N-1 关系。

从CF源码中可以找到CFString的源码,即可以看到NSString内部重写的hash方法。

这句话的大意是:这个字符串的大小如果小于等于96,则保证哈希的安全;如果大小大于96,则会大幅度增加哈希冲突的概率。

2. 与哈希表配合,hash方法对应哈希表的哈希函数,但返回值需要经过转换对应存储索引(一般通过取余)。此时hash方法的设计就非常重要,应做到输出哈希码的平均分散化,否则会导致数据堆叠,增大哈希冲突概率。

常见的NSDictionary和NSSet内部就是哈希表,NSDictionary会使用Key作为哈希表的Key,NSSet使用元素作为Key。它们会对Key对象调用hash方法获得初步的哈希码,然后经过转变成为存储区域的索引(下标)。

NSDictionary和NSSet因为都使用哈希表存储,因此存储元素都是无序的。

NSDictionary和NSSet在插入新元素时具体步骤如下:

带你整理面试过程中关于Redis 中的字典及 rehash的相关知识点

文章目录

一、Redis 中的字典

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

Redis 中的字典相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 “数组 + 链表” 的 链地址法 来解决部分 哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。

1. 哈希表

Redis字典所使用的哈希表由dict.h/dictht结构定义:

以上是关于iOS中的哈希表的主要内容,如果未能解决你的问题,请参考以下文章

数据结构--哈希表

哈希表中的查找

哈希表

带你整理面试过程中关于Redis 中的字典及 rehash的相关知识点

JavaScript 中的哈希表

WebService 中的静态哈希表