我觉得需要先梳理相关的概念,国内部分的教材,概念可能因为计算机理论的快速发展和更新而变得比较模糊和陈旧(有些教材因为编纂比较早,可能现在来看有些内容就不太合适了)
所以,结合相关书籍资料,以及维基百科,我个人理解如下,希望有帮助吧:
[相关术语]:
列表,又称序列,表示一组可数的有序的数值。
本身可以对应多种数据结构,其中最具代表性的就是数组(array)和链表(linked list)
哈希表,又称为散列表,是一种比搜索树(search trees)或者任何其他的表查找结构(table lookup structure)更有效数据结构,一般应用于关联数组、数据库索引、高速缓存和集合等方面
哈希序列,又称为散列序列,是一个数组或链表,其元素或结点是哈希值(hash value)的数据结构,一般应用于快速查表(哈希表),分布式数据库(分布式哈希表)以及保证数据完整性等方面
// 在 wiki 上特别指出了: hash list 和 hash table 不是一个东西, “(hash table) Not to be confused with Hash list or Hash tree.”
// 参考: https://stackoverflow.com/questions/2974597/hash-table-vs-hash-list-vs-hash-tree
// 个人理解是 hash table 的概念更广义一些,泛指一切 key 为 hashcode 的键值对形式的数据结构:,当文献强调 hash table 时,可能会比较注重 hashcode 如何计算,均匀性的内容
// 而 hash list、hash tree 的概念和具体的应用联系紧密,可以认为是 hash table 概念的数组/链表结构、树结构的具体实现,当文献强调 hash list、hash tree 时,可能会比较注重冲突处理、动态调整的内容
// 那么,下面的概念性的描述一般都是针对 hash table 的,但是具体的一些问题,以及对应问题的处理是需要结合其具体的数据结构来看的
// 比如: 用数组实现 hash table 和用树实现 hash table ,遇到的问题,以及处理方法肯定不同
哈希表是一种通过 keys 对 value 的映射,来实现数据访问的数据结构,而这种映射一般分为两个阶段:
1)将关键字 key 作为自变量, 通过一定的函数关系,计算出的因变量作为 value 的索引(index)
2)如果该索引指向的空间上没有存储数据,则将 value 插入,否则,称为产生了一个冲突(collision),需要对冲突进行处理,之后找到新的没有数据的空间,再将 value 插入
// 这种函数关系是通过哈希函数(hash function)来实现的, 即将任意长度的数据映射为较短的固定长度的数据,该数据(也就是
index)也被称为哈希值(hash values / hash codes)// 通过索引,可以在数组中找到一个元素,通过该元素可以找到 key 所对应的 value, value
也被称为记录(entry),该数组被称为桶数组(buckets array)或者关联数组(associative array)// 桶数组的元素被称为桶(bucket)或者槽(slot)
冲突处理,当key的集合很大的时候,根据生日问题(birthday problem)原理,哈希冲突实际上是不可避免的,所以几乎所有的哈希表都会有对于冲突的解决策略,常见的方法有三种;
1)拉链法(separate chaining):也被称为开放散列法(open hashing)或封闭定址法(closed addressing)
2)开放定址法(open addressing),也被称为封闭散列(closed hashing)
3)建立公共溢出区(building a public overflow area)
4)其他方法:
①联合哈希法(coalesced hashing)
②疯狂哈希法(cuckoo hashing)
③跳房子哈希法(hopscotch hashing)
④罗宾汉哈希法(robin hood hashing)
⑤二选哈希法(2-choice hashing)
开放散列,也称开放哈希,所有的纪录都不存储在桶数组中,每个桶都是独立的,每个桶都会对应一组有序的记录,存储这些记录的结构有3种
1)链表结构(separate chaining with linked lists),存储记录的是链表,即每个桶存一个指针,指向一个链表,链表中的结点(node)就是记录,该链表也被称为桶链(bucket chains)
2)列表头元素结构(separate chaining with list head cells),每个桶存的是该桶中的第一个记录,每个记录通过 next 指针和后面的记录相连,和 linked list 相比少了一个头指针
3)其他结构(separate chaining with other structures),将桶链替换为其他的数据结构,比如自平衡树(self-balancing tree),比如动态数组(动态数组每次增长固定的大小)
1)封闭散列,也称封闭哈希,所有的记录都存储在桶数组中,当一个新的纪录插入桶数组时,必须对数组进行探测(probe)
2)探测是通过探测序列(probe sequence)实现的,用于找到一个没有存储数据的槽,常用的探测序列有:
①线性探测(linear probing)
②二次探测(quadratic probing)
③伪随机探测(pseudo random probing)
④双重散列探测(double hashing probing)
动态调整,是针对封闭散列的,有两种情况:
1)当插入新记录到桶数组时,计算桶数组的负载因子(load factor),如果大于一定阈值时,对桶数组的大小进行扩容
2)当从桶数组中删除记录时,计算桶数组的负载因子(load factor),如果小于一定阈值时,对桶数组的大小进行缩容
// 扩容和缩容,都需要将旧记录迁移到新的地址上,这就需要对 key 进行重新映射,这种重新映射被称为再哈希(rehash)
一个评估哈希表的关键统计数据,被定义为:load factor = n / k, n 是记录的数量,k 是桶的数量
1)随着负载因子的扩大,出现冲突的概率会越来越大,所以当超过一定阈值时,需要扩容,避免哈希表因为频繁处理冲突而越来越慢
2)随着负载因子的缩小,桶数组中空着的槽就越来越多,所以当小过一定阈值时,需要缩容,避免空槽飙升导致的内存浪费
序列化,也被称为编组(marshalling)是将数据结构或对象状态转换为可存储格式的过程(比如将内存中加载的数据比如 map的一个kv对,转换保存到本地磁盘文件中,比如文件中的一行字符串 stirng \t string(int 2 stirng))序列化的逆过程被称为反序列化(deserialization )或解散(unmarshalling)
[优缺点]
由上面的术语可知:
1)优点:
①对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整的开销)
②由于记录存储在结点中,而结点是动态分配,不会造成内存的浪费,所以尤其适合那种记录本身尺寸(size)很大的情况,因为此时指针的开销可以忽略不计了
③删除记录时,比较方便,直接通过指针操作即可
2)缺点:
①存储的记录是随机分布在内存中的,这样在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销
②如果所有的 key-value 对是可以提前预知,并之后不会发生变化时(即不允许插入和删除),可以人为创建一个不会产生冲突的完美哈希函数(perfect hash function),此时封闭散列的性能将远高于开放散列
③由于使用指针,记录不容易进行序列化(serialize)操作
1)优点:
①记录更容易进行序列化(serialize)操作
②如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的
2)缺点:
①存储记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升,这在实时或者交互式应用中可能会是一个严重的缺陷
②使用探测序列,有可能其计算的时间成本过高,导致哈希表的处理性能降低
③由于记录是存放在桶数组中的,而桶数组必然存在空槽,所以当记录本身尺寸(size)很大并且记录总数规模很大时,空槽占用的空间会导致明显的内存浪费
④删除记录时,比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作。
1)优点:
记录数据量很大的时候,处理记录的速度很快,平均操作时间是一个不太大的常数
2)缺点:
①好的哈希函数(good hash function)的计算成本有可能会显著高于线性表或者搜索树在查找时的内部循环成本,所以当数据量非常小的时候,哈希表是低效的
②哈希表按照 key 对 value 有序枚举(ordered enumeration, 或者称有序遍历)是比较麻烦的(比如:相比于有序搜索树),需要先取出所有记录再进行额外的排序
③哈希表处理冲突的机制本身可能就是一个缺陷,攻击者可以通过精心构造数据,来实现处理冲突的最坏情况。即:每次都出现冲突,甚至每次都出现多次冲突(针对封闭散列的探测),以此来大幅度降低哈希表的性能。这种攻击也被称为基于哈希冲突的拒绝服务攻击(Hashtable collisions as DOS attack)
// 好的哈希函数是指产生的哈希值是均匀(uniform)分布的,即可均匀分布在桶数组中
// 最坏的情况下插入数据被称作哈希表的退化(degenerate)
// 哈希表拒绝服务攻击,可以参考:PHP哈希表碰撞攻击原理 - 文章 - 伯乐在线