散列 哈希

散列函数的设计
过于复杂的散列函数,势必会消耗很多计算时间,散列函数生成的值要尽可能随机并且均匀分布。
如今的一些散列函数:直接寻址法、平方取中法、折叠法、随机数法等。
直接寻址法:直接定址法是以数据元素关键字k本身或它的线性函数作为它的哈希地址。键字的元素很少是连续的。用该方法产生的哈希表会造成空间大量的浪费。
平方取中法:先取关键字的平方,然后根据可使用空间的大小,选取平方数是中间几位为哈希地址。
哈希函数 H(key)=“key平方的中间几位”因为这种方法的原理是通过取平方扩大差别,平方值的中间几位和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。
折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)。
随机数法:除留余数法:取关键字被某个不大于散列表 表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p
。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取,素数,质数(减少散列冲突),或m,若p选的不好,容易产生同义词。

哈希算法:将任意长度的二进制值串映射为固定长度的二进制值串。
要求:
从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法);
对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同;
散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;
哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。
应用一:安全加密
MD5 消息摘要算法 SHA(Secure Hash Algorithm,安全散列算法)DES(Data Encryption Standard,数据加密标准)、AES(Advanced Encryption Standard,高级加密标准)。
应用二:唯一标识,图片搜索
应用三:数据校验,下载文件拼装校验
应用四:散列函数,
应用五:负载均衡,轮询,随机,加权轮询。通过哈希算法计算数据,分配访问的机器,这样统一用户访问的机器都是同一台。
应用六:数据分片,对数据分片,计算哈希值,看看该数据应该由那台机器处理
应用七:分布式存储

一致性哈希算法(当要增加节点时)https://www.sohu.com/a/158141377_479559 //漫画理解
假设我们有 k 个机器,数据的哈希值的范围是 [0, MAX]。我们将整个范围划分成 m 个小区间(m 远大于 k),每个机器负责 m/k 个小区间。当有新机器加入的时候,我们就将某几个小区间的数据,从原来的机器中搬移到新的机器中。这样,既不用全部重新哈希、搬移数据,也保持了各个机器上数据数量的均衡。

散列表
用的就是数组支持按照下标随机访问的时候,时间复杂度是 O(1) 的特性。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。

散列函数,顾名思义,它是一个函数。我们可以把它定义成hash(key),其中 key 表示元素的键值,hash(key)的值表示经过散列函数计算得到的散列值。

散列函数设计的基本要求:
散列函数计算得到的散列值是一个非负整数;
如果 key1 = key2,那 hash(key1) ==hash(key2);
如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。

散列冲突
1.开放寻址法(适用于数据量比较小,装载因子小的场景)
优点:散列表中的数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。而且,这种方法实现的散列表,序列化起来比较简单。
缺点:用开放寻址法解决冲突的散列表,删除数据的时候比较麻烦,需要特殊标记已经删除掉的数据。而且,在开放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。装载因子的上限不能太大。
如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。
线性探测(最坏的时间复杂度位O(n))
当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
在这里插入图片描述查找元素的过程
找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。
删除元素的过程
找到之后直接删除会有问题(查到的时候发现有位置空,会认定原来存在的数据不存在)
我们可以将删除的元素,特殊标记为 deleted。当线性探测查找的时候,遇到标记为 deleted 的空间,并不是停下来,而是继续往下探测。

二次探测
二次探测探测的步长就变成了原来的“二次方”
,也就是说,它探测的下标序列就是 hash(key)+0,hash(key)+1的平方,hash(key)+2的平方…

双重散列
不仅要使用一个散列函数。我们使用一组散列函数
hash1(key),hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

尽可能的保证散列表中有一定比例的空闲槽位。
装载因子

散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
当装载因子过大时,我们也可以进行动态扩容,重新申请一个更大的散列表,将数据搬移到这个新散列表中。
数据的搬移需要通过散列函数重新计数每个数据的存储位置。
如:在这里插入图片描述
插入的时间复杂度:如果不需要动态扩容,所以时O(1),如果需要动态扩容,所以时间复杂度时O(n),摊还分析之后,时间复杂度时O(1)。插入数据也可以这样做:当超过阈值的时候,只申请空间,然后每次插入新的数据时,把新数据放入新的空间,同时在原散列表中拿出一个数据放入新的散列表。这样每次插入操作都可以很快速。查找的话先从新散列表查找,然后再从旧得散列表查找。
在这里插入图片描述
如果对空间有要求,在删除较多数据之后,可以启动缩容。

2.链表法(更常用)
优点:链表法对内存的利用率比开放寻址法要高。,只要散列函数的值随机均匀,即便装载因子变成 10,也就是链表的长度变长了而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。
缺点:链表因为要存储指针,所以对于比较小的对象的存储,是比较消耗内存的,而且,因为链表中的结点是零散分布在内存中的,不是连续的,所以对 CPU 缓存是不友好的,这方面对于执行效率也有一定的影响。如果存放的时大对象,指针的内存消耗可以忽略。

在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
在这里插入图片描述
插入
只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。
查找和删除
同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。
这两个操作的时间复杂度跟链表的长度 k 成正比,也就是 O(k)。对于散列比较均匀的散列函数来说,
理论上讲,k=n/m,其中 n 表示散列中数据的个数,m 表示散列表中“槽”的个数。

散列表碰撞攻击
通过一定设计的数据,将数据存入散列表,使所有数据都散列到同一个槽中,此时散列表就会退化成链表。
但是如果对链表法稍微改造,可以实现一个更加高效的散列表。那就是,我们将链表法中的链表改造为其他高效的动态数据结构,比如跳表、红黑树。这样,即便出现散列冲突,极端情况下,所有的数据都散列到同一个桶内,那最终退化成的散列表的查找时间也只不过是 O(logn)。这样也就有效避免了前面讲到的散列碰撞攻击。

HashMap的设计(数据+链表+红黑树)
1.初始大小
HashMap 默认的初始大小是 16,如果事先知道大概的数据量有多大,可以通过修改默认初始大小,减少动态扩容的次数,这样会大大提高 HashMap 的性能。
2. 装载因子和动态扩容
最大装载因子默认是 0.75,当 HashMap 中元素个数超过 0.75*capacity(capacity 表示散列表的容量)的时候,就会启动扩容,每次扩容都会扩容为原来的两倍大小。
3. 散列冲突解决方法
HashMap 底层采用链表法来解决冲突。一旦出现拉链过长(默认超过 8)链表就转换为红黑树。我们可以利用红黑树快速增删改查的特点,提高 HashMap 的性能。当红黑树结点个数少于 8 个的时候,又会将红黑树转化为链表。

int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}

先补充下老师使用的这段代码的一些问题:在JDK HashMap源码中,是分两步走的:

  1. hash值的计算,源码如下:
    static final int hash(Object key) {
    int hash;
    return key == null ? 0 : (hash = key.hashCode()) ^ hash >>> 16;
    }

  2. 在插入或查找的时候,计算Key被映射到桶的位置:
    int index = hash(key) & (capacity - 1)

JDK HashMap中hash函数的设计,确实很巧妙:
首先hashcode本身是个32位整型值,在系统中,这个值对于不同的对象必须保证唯一(JAVA规范),这也是大家常说的,重写equals必须重写hashcode的重要原因。

获取对象的hashcode以后,先进行移位运算,然后再和自己做异或运算,即:hashcode ^ (hashcode >>> 16),这一步甚是巧妙,是将高16位移到低16位,这样计算出来的整型值将“具有”高位和低位的性质加粗样式
最后,用hash表当前的容量减去一,再和刚刚计算出来的整型值做位与运算。进行位与运算,很好理解,是为了计算出数组中的位置。但这里有个问题:
为什么要用容量减去一?
因为 A % B = A & (B - 1),所以,(h ^ (h >>> 16)) & (capitity -1) = (h ^ (h >>> 16)) % capitity,可以看出这里本质上是使用了除留余数法
综上,可以看出,hashcode的随机性,加上移位异或算法,得到一个非常随机的hash值,再通过「除留余数法」,得到index,整体的设计过程与老师所说的“散列函数”设计原则非常吻合!

**一致性哈希:**原文 :https://www.cnblogs.com/lpfuture/p/5796398.html
一致性Hash性质
  考虑到分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来,如何保证当系统的节点数目发生变化时仍然能够对外提供良好的服务,这是值得考虑的,尤其实在设计分布式缓存系统时,如果某台服务器失效,对于整个系统来说如果不采用合适的算法来保证一致性,那么缓存于系统中的所有数据都可能会失效(即由于系统节点数目变少,客户端在请求某一对象时需要重新计算其hash值(通常与系统中的节点数目有关),由于hash值已经改变,所以很可能找不到保存该对象的服务器节点),因此一致性hash就显得至关重要,良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面:
平衡性(Balance)
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
单调性(Monotonicity)
**单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。**简单的哈希算法往往不能满足单调性的要求,哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。而在P2P系统内,缓冲的变化等价于Peer加入或退出系统,这一情况在P2P系统中会频繁发生,因此会带来极大计算和传输负荷。单调性就是要求哈希算法能够应对这种情况。
分散性(Spread)
在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
负载(Load)
负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
平滑性(Smoothness)
平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。
在这里插入图片描述

设计的一个工业级的散列函数
要求:
支持快速的查询、插入、删除操作;
内存占用合理,不能浪费过多的内存空间;
性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况;
设计思路:
设计一个合适的散列函数;
定义装载因子阈值,并且设计动态扩容策略;
选择合适的散列冲突解决方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值